From e9fad0bb0fada3b987461c0e232bc4a6efea3304 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 17 Sep 2026 09:04:09 +0800 Subject: [PATCH] fix(indexeddb): snapshot writes before evaluating keys --- .../wpt-cross-current/passed-cases.txt | 24 ++ .../fixtures/indexeddb-write-clone.js | 216 ++++++++++++++++++ .../tests/web_apis/indexed_db_transaction.rs | 21 ++ moli-indexeddb/src/manager.rs | 18 +- moli-indexeddb/src/tests.rs | 72 ++++++ moli-indexeddb/src/transaction.rs | 22 +- .../src/context_bootstrap/indexed_db/core.rs | 17 +- .../indexed_db/core/clone.rs | 18 +- .../indexed_db/core/keys/key_path.rs | 20 -- .../indexed_db/core/keys/value.rs | 5 + .../indexed_db/core/keys/write.rs | 8 +- .../indexed_db/core/keys/write/clone.rs | 49 +++- .../indexed_db/core/keys/write/extraction.rs | 156 ++++++++----- .../indexed_db/core/keys/write/injection.rs | 110 ++++----- .../indexed_db/core/keys/write/prepare.rs | 120 +++++----- .../context_bootstrap/indexed_db/cursor.rs | 23 +- .../indexed_db/cursor/mutation/delete.rs | 19 +- .../indexed_db/cursor/mutation/update.rs | 147 ++++-------- .../indexed_db/cursor/state.rs | 67 +++--- .../src/context_bootstrap/indexed_db/mod.rs | 1 - .../indexed_db/operation_state.rs | 35 +-- .../context_bootstrap/indexed_db/stores.rs | 20 +- .../indexed_db/stores/object_store/common.rs | 12 +- .../object_store/write/put_add/operation.rs | 60 +++-- .../write/put_add/operation/enqueue.rs | 25 -- .../write/put_add/operation/execute.rs | 107 --------- .../src/context_bootstrap/indexed_db/tasks.rs | 15 +- .../operations/flush/object_store/write.rs | 9 +- .../operations/object_store/write/put_add.rs | 57 ++--- .../src/context_bootstrap/indexed_db/types.rs | 16 +- .../src/script_vm/tests/indexed_db.rs | 47 ++-- 31 files changed, 860 insertions(+), 676 deletions(-) create mode 100644 moli-core/tests/web_apis/fixtures/indexeddb-write-clone.js delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation/enqueue.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation/execute.rs diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 19e5c61c34..9162865b81 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -11,6 +11,8 @@ FileAPI/idlharness.html FileAPI/url/url-lifetime.html IndexedDB/abort-in-initial-upgradeneeded.any.js?moli-wpt-any=dedicatedworker IndexedDB/abort-in-initial-upgradeneeded.any.js?moli-wpt-any=window +IndexedDB/clone-before-keypath-eval.any.js?moli-wpt-any=dedicatedworker +IndexedDB/clone-before-keypath-eval.any.js?moli-wpt-any=window IndexedDB/close-in-upgradeneeded.any.js?moli-wpt-any=dedicatedworker IndexedDB/close-in-upgradeneeded.any.js?moli-wpt-any=window IndexedDB/database-names-by-origin.html @@ -25,6 +27,16 @@ IndexedDB/fire-upgradeneeded-event-exception.any.js?moli-wpt-any=window IndexedDB/idb-explicit-commit.any.js?moli-wpt-any=dedicatedworker IndexedDB/idb-explicit-commit.any.js?moli-wpt-any=window IndexedDB/idb_webworkers.htm +IndexedDB/idbcursor-delete-exception-order.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbcursor-delete-exception-order.any.js?moli-wpt-any=window +IndexedDB/idbcursor-update-exception-order.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbcursor-update-exception-order.any.js?moli-wpt-any=window +IndexedDB/idbcursor_delete_index.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbcursor_delete_index.any.js?moli-wpt-any=window +IndexedDB/idbcursor_delete_objectstore.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbcursor_delete_objectstore.any.js?moli-wpt-any=window +IndexedDB/idbcursor_update_objectstore.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbcursor_update_objectstore.any.js?moli-wpt-any=window IndexedDB/idbdatabase-createObjectStore-exception-order.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbdatabase-createObjectStore-exception-order.any.js?moli-wpt-any=window IndexedDB/idbdatabase-transaction-exception-order.any.js?moli-wpt-any=dedicatedworker @@ -47,9 +59,15 @@ IndexedDB/idbfactory_deleteDatabase.any.js?moli-wpt-any=window IndexedDB/idbfactory_open.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbfactory_open.any.js?moli-wpt-any=window IndexedDB/idbindex-cross-realm-methods.html +IndexedDB/idbobjectstore-add-put-exception-order.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbobjectstore-add-put-exception-order.any.js?moli-wpt-any=window IndexedDB/idbobjectstore-cross-realm-methods.html IndexedDB/idbobjectstore-deleteIndex-exception-order.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbobjectstore-deleteIndex-exception-order.any.js?moli-wpt-any=window +IndexedDB/idbobjectstore_add.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbobjectstore_add.any.js?moli-wpt-any=window +IndexedDB/idbobjectstore_put.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbobjectstore_put.any.js?moli-wpt-any=window IndexedDB/idbrequest_error.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbrequest_error.any.js?moli-wpt-any=window IndexedDB/idbrequest_result.any.js?moli-wpt-any=dedicatedworker @@ -58,11 +76,17 @@ IndexedDB/idbtransaction_abort.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbtransaction_abort.any.js?moli-wpt-any=window IndexedDB/idbversionchangeevent.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbversionchangeevent.any.js?moli-wpt-any=window +IndexedDB/keypath-exceptions.any.js?moli-wpt-any=dedicatedworker +IndexedDB/keypath-exceptions.any.js?moli-wpt-any=window +IndexedDB/keypath-special-identifiers.any.js?moli-wpt-any=dedicatedworker +IndexedDB/keypath-special-identifiers.any.js?moli-wpt-any=window IndexedDB/keypath_invalid.any.js?moli-wpt-any=dedicatedworker IndexedDB/keypath_invalid.any.js?moli-wpt-any=window IndexedDB/open-request-queue.any.js?moli-wpt-any=dedicatedworker IndexedDB/open-request-queue.any.js?moli-wpt-any=window IndexedDB/ready-state-destroyed-execution-context.html +IndexedDB/structured-clone-transaction-state.any.js?moli-wpt-any=dedicatedworker +IndexedDB/structured-clone-transaction-state.any.js?moli-wpt-any=window IndexedDB/transaction-abort-index-metadata-revert.any.js?moli-wpt-any=dedicatedworker IndexedDB/transaction-abort-index-metadata-revert.any.js?moli-wpt-any=window IndexedDB/transaction-abort-multiple-metadata-revert.any.js?moli-wpt-any=dedicatedworker diff --git a/moli-core/tests/web_apis/fixtures/indexeddb-write-clone.js b/moli-core/tests/web_apis/fixtures/indexeddb-write-clone.js new file mode 100644 index 0000000000..4ac51aa503 --- /dev/null +++ b/moli-core/tests/web_apis/fixtures/indexeddb-write-clone.js @@ -0,0 +1,216 @@ +'use strict'; +globalThis.writeChecks = []; +function writeCheck(name, pass, detail = '') { + writeChecks.push({name, pass: !!pass, detail: String(detail)}); +} +function writeThrows(name, callback, expected) { + try { callback(); writeCheck(name, false, 'no exception'); } + catch (error) { writeCheck(name, typeof expected === 'string' ? error.name === expected : error === expected, error); } +} +function writeRequest(request) { + return new Promise(resolve => { + request.onsuccess = () => resolve(request.result); + request.onerror = event => { event.preventDefault(); resolve({requestError: request.error.name}); }; + }); +} +function writeTransaction(transaction) { + return new Promise(resolve => { + transaction.oncomplete = () => resolve('complete'); + transaction.onabort = () => resolve('abort'); + transaction.onerror = event => event.preventDefault(); + }); +} +async function writeDatabase(name) { + const request = indexedDB.open(name, 1); + request.onupgradeneeded = () => { + const db = request.result; + const store = db.createObjectStore('inline', {keyPath: 'id', autoIncrement: true}); + store.createIndex('byTag', 'tag', {unique: true}); + db.createObjectStore('nested', {keyPath: 'a.b.id', autoIncrement: true}); + db.createObjectStore('out', {autoIncrement: true}); + db.createObjectStore('length', {keyPath: 'text.length'}); + db.createObjectStore('special', {keyPath: 'blob.type'}); + }; + return writeRequest(request); +} +async function writeCloneProbe(prefix = 'write-clone-probe') { + for (const method of ['add', 'put']) { + for (const queued of [false, true]) { + const label = `${method}-${queued ? 'queued' : 'started'}`; + const db = await writeDatabase(`${prefix}-${label}`); + const stores = Array.from(db.objectStoreNames); + const blocker = queued ? db.transaction(stores, 'readwrite') : null; + const blockerDone = blocker && writeTransaction(blocker); + if (blocker) blocker.objectStore('out').get(0); + const tx = db.transaction(stores, 'readwrite'); + const done = writeTransaction(tx); + const store = tx.objectStore('inline'); + const out = tx.objectStore('out'); + const nested = tx.objectStore('nested'); + let idReads = 0, tagReads = 0; + const record = { + get id() { idReads++; return 10; }, + get tag() { + tagReads++; + writeThrows(`${label}: inactive during clone`, () => out.get(0), 'TransactionInactiveError'); + writeThrows(`${label}: commit during clone`, () => tx.commit(), 'InvalidStateError'); + return 'original'; + }, + data: {value: 'before'} + }; + const request = store[method](record); + const saved = writeRequest(request); + writeCheck(`${label}: getters synchronous and once`, idReads === 1 && tagReads === 1, `${idReads}/${tagReads}`); + writeCheck(`${label}: accepted request pending`, request.readyState === 'pending'); + record.data.value = 'after'; + const sentinel = {sentinel: label}; + writeThrows(`${label}: exact getter exception`, () => out[method]({get value() { throw sentinel; }}, 'throws'), sentinel); + writeThrows(`${label}: uncloneable value`, () => out[method](() => {}, 'function'), 'DataCloneError'); + writeThrows(`${label}: invalid inline key`, () => store[method]({id: null, tag: 'invalid'}), 'DataError'); + writeThrows(`${label}: invalid explicit key`, () => out[method]({}, null), 'DataError'); + writeThrows(`${label}: inline explicit key before clone`, () => store[method]({get id() { throw sentinel; }}, 1), 'DataError'); + writeThrows(`${label}: injection through undefined`, () => nested[method]({a: undefined}), 'DataError'); + writeThrows(`${label}: injection through primitive`, () => nested[method]({a: {b: 'text'}}), 'DataError'); + const throwingKey = []; + Object.defineProperty(throwingKey, '0', {get() { throw sentinel; }, configurable: true}); + writeThrows(`${label}: exact explicit array getter`, () => out[method]({}, throwingKey), sentinel); + let prototypeReads = 0; + Object.defineProperty(Object.prototype, 'a', {configurable: true, get() { prototypeReads++; throw sentinel; }}); + let injected; + try { injected = writeRequest(nested[method]({})); } + finally { delete Object.prototype.a; } + writeCheck(`${label}: injection ignores prototype`, prototypeReads === 0, prototypeReads); + const inherited = Object.create({id: 999}); + inherited.tag = 'generated'; + const generated = writeRequest(store[method](inherited)); + const conflict = writeRequest(store[method]({tag: 'original'})); + const afterConflict = writeRequest(store[method]({tag: 'after-conflict'})); + const sparse = Array(1); + Object.setPrototypeOf(sparse, Object.create(Array.prototype, {0: {get() { throw sentinel; }}})); + writeThrows(`${label}: array holes ignore inherited getters`, () => out[method]({}, sparse), 'DataError'); + for (const thrown of [null, undefined, 17, 'sentinel']) { + try { out[method]({get value() { throw thrown; }}, 'primitive-throw'); writeCheck(`${label}: primitive exception ${String(thrown)}`, false); } + catch (error) { writeCheck(`${label}: primitive exception ${String(thrown)}`, error === thrown, String(error)); } + } + const arrayKey = ['before', [2]]; + const arrayWrite = writeRequest(out[method]({data: 'array'}, arrayKey)); + arrayKey[0] = 'after'; arrayKey[1][0] = 9; + const utf16 = writeRequest(tx.objectStore('length')[method]({text: 'A\u{1F600}'})); + let blobReads = 0; + const oldType = Object.getOwnPropertyDescriptor(Blob.prototype, 'type'); + Object.defineProperty(Blob.prototype, 'type', {configurable: true, get() { blobReads++; throw sentinel; }}); + let special; + try { special = writeRequest(tx.objectStore('special')[method]({blob: new Blob(['x'], {type: 'text/plain'})})); } + finally { Object.defineProperty(Blob.prototype, 'type', oldType); } + writeCheck(`${label}: Blob key uses native data`, blobReads === 0, blobReads); + Object.defineProperties(store, { + keyPath: {configurable: true, get() { throw sentinel; }}, + autoIncrement: {configurable: true, get() { throw sentinel; }} + }); + let native; + try { native = writeRequest(store[method]({id: 20, tag: 'metadata'})); } + catch (error) { writeCheck(`${label}: native metadata write`, false, error); } + finally { delete store.keyPath; delete store.autoIncrement; } + const status = await done; + if (blockerDone) await blockerDone; + writeCheck(`${label}: transaction completes after caught exceptions`, status === 'complete', status); + writeCheck(`${label}: stored primary key`, await saved === 10); + writeCheck(`${label}: no delayed getters`, idReads === 1 && tagReads === 1, `${idReads}/${tagReads}`); + writeCheck(`${label}: failed injection preserves generator`, await injected === 1); + writeCheck(`${label}: missing inherited key generates`, await generated === 11); + writeCheck(`${label}: unique constraint rejects generated write`, (await conflict)?.requestError === 'ConstraintError'); + writeCheck(`${label}: constraint failure preserves generator`, await afterConflict === 12); + writeCheck(`${label}: array key snapshot`, JSON.stringify(await arrayWrite) === '["before",[2]]'); + writeCheck(`${label}: String length counts UTF-16`, await utf16 === 3); + writeCheck(`${label}: Blob type extracted`, await special === 'text/plain'); + if (native) writeCheck(`${label}: native metadata write`, await native === 20); + const read = db.transaction(stores); + const readDone = writeTransaction(read); + const readValue = writeRequest(read.objectStore('inline').get(10)); + const readNested = writeRequest(read.objectStore('nested').get(1)); + const readIndex = writeRequest(read.objectStore('inline').index('byTag').getKey('original')); + writeCheck(`${label}: value snapshot`, (await readValue)?.data?.value === 'before'); + writeCheck(`${label}: injected nested key`, (await readNested)?.a?.b?.id === 1); + writeCheck(`${label}: index uses same snapshot`, await readIndex === 10); + await readDone; + db.close(); + } + } + const db = await writeDatabase(`${prefix}-cursor`); + let tx = db.transaction('inline', 'readwrite'); + let done = writeTransaction(tx); + tx.objectStore('inline').put({id: 1, tag: 'one'}); + tx.objectStore('inline').put({id: 2, tag: 'two'}); + await done; + tx = db.transaction('inline', 'readwrite'); done = writeTransaction(tx); + let store = tx.objectStore('inline'); + const cursor = await writeRequest(store.openCursor(1)); + const oldValue = cursor.value; + let reads = 0; + const newValue = {get id() { reads++; return 1; }, tag: 'updated'}; + const update = writeRequest(cursor.update(newValue)); + writeCheck('cursor: clone getter once', reads === 1, reads); + writeCheck('cursor: cached value unchanged', cursor.value === oldValue && oldValue.tag === 'one'); + newValue.tag = 'mutated'; + writeThrows('cursor: reject changed inline key', () => cursor.update({id: 3}), 'DataError'); + const exact = {cursor: true}; + writeThrows('cursor: exact clone exception', () => cursor.update({get id() { throw exact; }}), exact); + const constrained = writeRequest(cursor.update({id: 1, tag: 'two'})); + const fresh = writeRequest(store.get(1)); + writeCheck('cursor: update result', await update === 1); + writeCheck('cursor: unique index constraint', (await constrained)?.requestError === 'ConstraintError'); + writeCheck('cursor: stored snapshot after failed unique write', (await fresh)?.tag === 'updated'); + await done; + tx = db.transaction('inline'); done = writeTransaction(tx); store = tx.objectStore('inline'); + let cloneCalls = 0; + writeThrows('readonly: reject before cloning', () => store.put({get id() { cloneCalls++; return 3; }}), 'ReadOnlyError'); + writeCheck('readonly: no getters', cloneCalls === 0, cloneCalls); + const keyCursor = await writeRequest(store.openKeyCursor()); + writeThrows('cursor: readonly before key-only', () => keyCursor.update({}), 'ReadOnlyError'); + writeThrows('cursor: readonly delete', () => keyCursor.delete(), 'ReadOnlyError'); + await done; + tx = db.transaction('inline', 'readwrite'); done = writeTransaction(tx); + const writeKeyCursor = await writeRequest(tx.objectStore('inline').openKeyCursor(1)); + writeThrows('cursor: readwrite key-only delete', () => writeKeyCursor.delete(), 'InvalidStateError'); + await done; + for (const method of ['add', 'put']) { + for (const queued of [false, true]) { + const label = `abort-${method}-${queued ? 'queued' : 'started'}`; + const blocker = queued ? db.transaction('out', 'readwrite') : null; + const blockerDone = blocker && writeTransaction(blocker); + if (blocker) blocker.objectStore('out').get(99); + const aborted = db.transaction('out', 'readwrite'); + const abortedDone = writeTransaction(aborted); + const target = aborted.objectStore('out'); + let request; + try { + request = target[method]({get value() { aborted.abort(); return 'cancelled'; }}, 99); + writeCheck(`${label}: returns request`, request instanceof IDBRequest && request.readyState === 'pending'); + } catch (error) { writeCheck(`${label}: returns request`, false, error); } + writeCheck(`${label}: transaction aborts`, await abortedDone === 'abort'); + writeThrows(`${label}: remains inactive`, () => target.get(99), 'TransactionInactiveError'); + if (blockerDone) await blockerDone; + const verification = db.transaction('out'); + const verificationDone = writeTransaction(verification); + writeCheck(`${label}: cancelled value absent`, await writeRequest(verification.objectStore('out').get(99)) === undefined); + await verificationDone; + } + } + tx = db.transaction('inline', 'readwrite'); done = writeTransaction(tx); + store = tx.objectStore('inline'); + const abortingCursor = await writeRequest(store.openCursor(1)); + try { + const request = abortingCursor.update({get id() { tx.abort(); return 1; }, tag: 'cancelled'}); + writeCheck('abort-cursor: returns request', request instanceof IDBRequest && request.readyState === 'pending'); + } catch (error) { writeCheck('abort-cursor: returns request', false, error); } + writeCheck('abort-cursor: transaction aborts', await done === 'abort'); + writeThrows('abort-cursor: remains inactive', () => store.get(1), 'TransactionInactiveError'); + tx = db.transaction(['inline', 'out'], 'readwrite'); done = writeTransaction(tx); + const preserved = writeRequest(tx.objectStore('inline').get(1)); + const generated = writeRequest(tx.objectStore('out').put({afterAbort: true})); + writeCheck('abort-cursor: stored value preserved', (await preserved)?.tag === 'updated'); + writeCheck('abort: generator not consumed', await generated === 1); + await done; + db.close(); + return {state: writeChecks.every(check => check.pass) ? 'pass' : 'fail', checks: writeChecks}; +} diff --git a/moli-core/tests/web_apis/indexed_db_transaction.rs b/moli-core/tests/web_apis/indexed_db_transaction.rs index ace3d6adbf..d0acf97f05 100644 --- a/moli-core/tests/web_apis/indexed_db_transaction.rs +++ b/moli-core/tests/web_apis/indexed_db_transaction.rs @@ -1,6 +1,27 @@ use super::event_dispatch::run_probe; use super::*; +#[tokio::test(flavor = "multi_thread")] +async fn indexed_db_writes_clone_before_key_paths_and_snapshot_queued_values() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let fixture = include_str!("fixtures/indexeddb-write-clone.js"); + for target in ["window", "child", "worker"] { + let source = format!( + "{fixture}\nwriteCloneProbe('write-clone-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: writeChecks}}));" + ); + let result = run_probe(&browser, &server, target, &source).await?; + assert_eq!(result["state"], "pass", "{target}: {result}"); + assert_eq!( + result["checks"].as_array().unwrap().len(), + 165, + "{target}: {result}" + ); + } + server.shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn indexed_db_schema_validation_preserves_key_paths_conversion_and_exception_order() -> Result<()> { diff --git a/moli-indexeddb/src/manager.rs b/moli-indexeddb/src/manager.rs index bda96ae57f..b10b6242ed 100644 --- a/moli-indexeddb/src/manager.rs +++ b/moli-indexeddb/src/manager.rs @@ -14,7 +14,9 @@ use crate::{ DatabaseData, DatabaseHandleState, IndexData, IndexedDbManager, ObjectStoreData, TransactionLifecycle, TransactionState, }, - transaction::{ensure_writeable, resolve_key, transaction_store, transaction_store_mut}, + transaction::{ + ensure_writeable, next_generated_key, resolve_key, transaction_store, transaction_store_mut, + }, usage::{database_usage_bytes, origin_usage_bytes, sum_usage}, validate_index_options, validate_object_store_options, }; @@ -680,6 +682,18 @@ impl IndexedDbManager { .collect()) } + /// Inspect the next generated key without consuming it. The binding uses + /// this to inject the key and validate indexes before an atomic write. + pub fn next_generated_key( + &mut self, + transaction: TransactionHandle, + store_name: &str, + ) -> Result { + let tx = self.active_transaction_mut(transaction)?; + ensure_writeable(tx)?; + next_generated_key(transaction_store(tx, store_name)?) + } + pub fn generate_key( &mut self, transaction: TransactionHandle, @@ -763,8 +777,10 @@ impl IndexedDbManager { let origin = tx.origin.clone(); let store = transaction_store_mut(tx, store_name)?; let previous_store = quota.map(|_| store.clone()); + let previous_counter = store.auto_increment_counter; let resolved_key = resolve_key(store, key)?; if add_only && store.records.contains_key(&resolved_key) { + store.auto_increment_counter = previous_counter; return Err(IndexedDbError::Constraint( "record already exists for key".to_owned(), )); diff --git a/moli-indexeddb/src/tests.rs b/moli-indexeddb/src/tests.rs index 345cf0999e..ec9aea2378 100644 --- a/moli-indexeddb/src/tests.rs +++ b/moli-indexeddb/src/tests.rs @@ -1753,6 +1753,78 @@ fn failed_open_does_not_block_later_delete_database() { .expect("delete should still succeed after failed open"); } +#[test] +fn generated_key_preview_and_failed_quota_write_leave_generator_unchanged() { + let dir = TestDir::new(); + let mut manager = IndexedDbManager::new(&dir.path).unwrap(); + let opened = manager + .open(OpenOptions { + origin: "https://example.com".to_owned(), + name: "key-preview".to_owned(), + version: None, + }) + .unwrap(); + let tx = opened.upgrade_transaction.unwrap(); + manager + .create_object_store( + tx, + "items", + ObjectStoreOptions { + key_path: None, + auto_increment: true, + }, + ) + .unwrap(); + assert_eq!( + manager.next_generated_key(tx, "items").unwrap(), + Key::Integer(1) + ); + assert_eq!( + manager.next_generated_key(tx, "items").unwrap(), + Key::Integer(1) + ); + assert_eq!( + manager + .put(tx, "items", Some(Key::Integer(1)), vec![1]) + .unwrap(), + Key::Integer(1) + ); + let error = manager + .put_with_quota( + tx, + "items", + Some(Key::Integer(20)), + vec![2], + IndexedDbQuotaCheck { + quota: 0, + non_indexed_db_usage: 0, + }, + ) + .unwrap_err(); + assert!(matches!(error, IndexedDbError::QuotaExceeded { .. })); + assert_eq!( + manager.next_generated_key(tx, "items").unwrap(), + Key::Integer(2) + ); + assert_eq!( + manager.put(tx, "items", None, vec![3]).unwrap(), + Key::Integer(2) + ); + manager.commit_transaction(tx).unwrap(); + let tx = manager + .begin_transaction( + opened.database, + &["items".to_owned()], + TransactionMode::ReadWrite, + ) + .unwrap(); + assert_eq!( + manager.next_generated_key(tx, "items").unwrap(), + Key::Integer(3) + ); + manager.abort_transaction(tx).unwrap(); +} + #[test] fn auto_increment_rejects_exhausted_safe_integer_range() { let dir = TestDir::new(); diff --git a/moli-indexeddb/src/transaction.rs b/moli-indexeddb/src/transaction.rs index 642dad1099..89fd68664d 100644 --- a/moli-indexeddb/src/transaction.rs +++ b/moli-indexeddb/src/transaction.rs @@ -64,15 +64,25 @@ pub(crate) fn resolve_key( return Ok(key); } if store.auto_increment { - if store.auto_increment_counter >= MAX_AUTO_INCREMENT_KEY { - return Err(IndexedDbError::Constraint( - "auto_increment key generator exceeded the maximum safe integer value".to_owned(), - )); - } + let key = next_generated_key(store)?; store.auto_increment_counter += 1; - return Ok(Key::Integer(store.auto_increment_counter as i64)); + return Ok(key); } Err(IndexedDbError::InvalidState( "a key is required when auto_increment is disabled".to_owned(), )) } + +pub(crate) fn next_generated_key(store: &ObjectStoreData) -> Result { + if !store.auto_increment { + return Err(IndexedDbError::InvalidState( + "a key is required when auto_increment is disabled".to_owned(), + )); + } + if store.auto_increment_counter >= MAX_AUTO_INCREMENT_KEY { + return Err(IndexedDbError::Constraint( + "auto_increment key generator exceeded the maximum safe integer value".to_owned(), + )); + } + Ok(Key::Integer((store.auto_increment_counter + 1) as i64)) +} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs index e0af32c1a5..36015eff2d 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs @@ -5,14 +5,15 @@ use super::{ INDEXED_DB_DATABASE_HANDLE_SLOT, INDEXED_DB_DATABASE_KEY_SLOT, INDEXED_DB_EVENT_LISTENERS_SLOT, INDEXED_DB_KEY_RANGE_MARKER_SLOT, INDEXED_DB_REQUEST_ERROR_SLOT, INDEXED_DB_REQUEST_READY_STATE_SLOT, INDEXED_DB_REQUEST_RESULT_SLOT, - INDEXED_DB_TRANSACTION_DB_KEY_SLOT, INDEXED_DB_TRANSACTION_FINISHED_SLOT, - INDEXED_DB_TRANSACTION_HANDLE_SLOT, IdbKeyRangeQuery, IndexEntry, IndexInfo, IndexedDbError, - IndexedDbExecutionOwner, IndexedDbExternalObject, IndexedDbManager, - IndexedDbObjectStoreMetadata, IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbValue, - IndexedDbWrapperKind, Key, KeyPath, ObjectStoreInfo, PreparedObjectStoreWrite, - PreparedObjectStoreWriteError, TransactionHandle, TransactionMode, - context_host_ptr_from_global_bridge, global_constructor_prototype, - indexed_db_database_store_metadata, indexed_db_object_store_metadata, indexed_db_runtime_array, + INDEXED_DB_TRANSACTION_ABORTED_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT, + INDEXED_DB_TRANSACTION_COMMITTING_SLOT, INDEXED_DB_TRANSACTION_DB_KEY_SLOT, + INDEXED_DB_TRANSACTION_FINISHED_SLOT, INDEXED_DB_TRANSACTION_HANDLE_SLOT, IdbKeyRangeQuery, + IndexEntry, IndexInfo, IndexedDbError, IndexedDbExecutionOwner, IndexedDbExternalObject, + IndexedDbManager, IndexedDbObjectStoreMetadata, IndexedDbRuntimeArray, IndexedDbStorageScope, + IndexedDbValue, IndexedDbWrapperKind, Key, KeyPath, ObjectStoreInfo, PreparedObjectStoreWrite, + TransactionHandle, TransactionMode, context_host_ptr_from_global_bridge, + global_constructor_prototype, indexed_db_database_store_metadata, + indexed_db_object_store_metadata, indexed_db_runtime_array, indexed_db_transaction_mode, indexed_db_typed_execution_owner, indexed_db_typed_storage_scope, new_null_prototype_object, object_bool_property, object_hidden_value, object_number_property, object_property_as_object, object_string_property, push_unique_object_to_indexed_db_runtime_array, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs index be6c1a1d3f..550fea0037 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs @@ -517,12 +517,11 @@ pub(in crate::context_bootstrap::indexed_db) fn serialize_js_value( scope: &mut v8::PinScope<'_, '_>, value: v8::Local<'_, v8::Value>, ) -> Option { - let mut should_throw_data_clone_error = false; let external_objects = Rc::new(RefCell::new(Vec::new())); let external_object_sources = Rc::new(RefCell::new(Vec::new())); let serialized = { let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); - let scope = try_catch.init(); + let mut scope = try_catch.init(); let context = scope.get_current_context(); let serializer = v8::ValueSerializer::new( &scope, @@ -535,8 +534,11 @@ pub(in crate::context_bootstrap::indexed_db) fn serialize_js_value( match serializer.write_value(context, value) { Some(true) => Some(serializer.release()), _ => { - should_throw_data_clone_error = - scope.has_caught() && scope.can_continue() && !scope.has_terminated(); + // The delegate creates DataCloneError for unsupported values. + // Exceptions from author getters must retain their identity. + if scope.has_caught() { + scope.rethrow(); + } None } } @@ -545,14 +547,6 @@ pub(in crate::context_bootstrap::indexed_db) fn serialize_js_value( let external_objects = external_objects.borrow().clone(); return Some(IndexedDbValue::new(wire_bytes, external_objects)); } - if should_throw_data_clone_error { - let exception = dom_exception_value( - scope, - "The value could not be cloned for IndexedDB storage.", - "DataCloneError", - ); - scope.throw_exception(exception); - } None } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/key_path.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/key_path.rs index 51bbfacce1..8f3db41216 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/key_path.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/key_path.rs @@ -43,23 +43,3 @@ pub(in crate::context_bootstrap::indexed_db) fn key_path_to_js_value<'s>( KeyPath::Sequence(values) => values.as_slice().to_v8_value(scope), } } - -pub(in crate::context_bootstrap::indexed_db) fn key_path_from_js_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, -) -> Option { - if value.is_null_or_undefined() { - return None; - } - if let Ok(array) = v8::Local::::try_from(value) { - let mut key_path = Vec::with_capacity(array.length() as usize); - for index in 0..array.length() { - let value = array.get_index(scope, index)?; - key_path.push(value.to_string(scope)?.to_rust_string_lossy(scope)); - } - return Some(KeyPath::Sequence(key_path)); - } - value - .to_string(scope) - .map(|value| KeyPath::String(value.to_rust_string_lossy(scope))) -} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/value.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/value.rs index edd508300e..854ae54414 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/value.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/value.rs @@ -48,6 +48,11 @@ fn parse_idb_key_with_depth( if let Ok(array) = v8::Local::::try_from(value) { let mut keys = Vec::with_capacity(array.length() as usize); for index in 0..array.length() { + let property = + v8_string(scope, &index.to_string()).ok_or("IndexedDB key allocation failed.")?; + if array.has_own_property(scope, property.into()) != Some(true) { + return Err("IndexedDB array keys must not contain missing entries."); + } let Some(entry) = array.get_index(scope, index) else { return Err("IndexedDB array keys must not contain missing entries."); }; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write.rs index f4b951a35a..a8a68da984 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write.rs @@ -5,10 +5,12 @@ mod extraction; mod injection; mod prepare; -use self::clone::clone_js_value; -use self::injection::inject_key_path_into_value; +use self::injection::can_inject_key; + +pub(in crate::context_bootstrap::indexed_db) use self::clone::clone_value_for_transaction; +pub(in crate::context_bootstrap::indexed_db) use self::injection::inject_key_path_into_value; pub(in crate::context_bootstrap::indexed_db) use self::extraction::{ - derive_object_store_key_from_value, extract_index_keys_from_value, + ExtractedKey, extract_index_keys_from_value, extract_key_from_value, }; pub(in crate::context_bootstrap::indexed_db) use self::prepare::prepare_object_store_write; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/clone.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/clone.rs index 65822f881c..5c866c41b5 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/clone.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/clone.rs @@ -1,9 +1,50 @@ use super::*; -pub(super) fn clone_js_value<'s>( +pub(in crate::context_bootstrap::indexed_db) fn clone_value_for_transaction<'s>( scope: &mut v8::PinScope<'s, '_>, + transaction: v8::Local<'s, v8::Object>, value: v8::Local<'s, v8::Value>, -) -> Option> { - let bytes = serialize_js_value(scope, value)?; - deserialize_js_value(scope, &bytes) +) -> Option<(v8::Local<'s, v8::Value>, IndexedDbValue)> { + set_indexed_db_slot_value( + scope, + transaction, + INDEXED_DB_TRANSACTION_ACTIVE_SLOT, + v8::Boolean::new(scope, false).into(), + ); + let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); + let mut scope = try_catch.init(); + // Restore activity even after a clone exception, but never resurrect a + // transaction that an author getter aborted during serialization. + let result = serialize_js_value(&mut scope, value) + .and_then(|bytes| deserialize_js_value(&mut scope, &bytes).map(|clone| (clone, bytes))); + if !object_bool_property( + &mut scope, + transaction, + INDEXED_DB_TRANSACTION_FINISHED_SLOT, + ) + .unwrap_or(false) + && !object_bool_property(&mut scope, transaction, INDEXED_DB_TRANSACTION_ABORTED_SLOT) + .unwrap_or(false) + && !object_bool_property( + &mut scope, + transaction, + INDEXED_DB_TRANSACTION_COMMITTING_SLOT, + ) + .unwrap_or(false) + { + let active = v8::Boolean::new(&scope, true); + set_indexed_db_slot_value( + &mut scope, + transaction, + INDEXED_DB_TRANSACTION_ACTIVE_SLOT, + active.into(), + ); + } + // Keep the exception caught until state restoration is complete: further + // V8 property accesses must not consume a pending rethrow. + if scope.has_caught() { + scope.rethrow(); + return None; + } + result } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/extraction.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/extraction.rs index 4b846c7310..5b79de1533 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/extraction.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/extraction.rs @@ -1,40 +1,64 @@ use super::*; +pub(in crate::context_bootstrap::indexed_db) enum ExtractedKey { + Key(Key), + Missing, + Invalid, +} + +pub(in crate::context_bootstrap::indexed_db) fn extract_key_from_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + key_path: &KeyPath, +) -> ExtractedKey { + match key_path { + KeyPath::Sequence(paths) => { + let mut keys = Vec::with_capacity(paths.len()); + for path in paths { + match extract_string_key(scope, value, path) { + ExtractedKey::Key(key) => keys.push(key), + result => return result, + } + } + ExtractedKey::Key(Key::Array(keys)) + } + KeyPath::String(path) => extract_string_key(scope, value, path), + } +} + +fn extract_string_key<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + path: &str, +) -> ExtractedKey { + let Some(value) = value_at_string_key_path(scope, value, path) else { + return ExtractedKey::Missing; + }; + match parse_idb_key(scope, value) { + Ok(Some(key)) => ExtractedKey::Key(key), + _ => ExtractedKey::Invalid, + } +} + pub(in crate::context_bootstrap::indexed_db) fn extract_index_keys_from_value<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, key_path: &KeyPath, multi_entry: bool, ) -> Vec { - if let KeyPath::Sequence(paths) = key_path { - let mut keys = Vec::with_capacity(paths.len()); - for path in paths { - let Some(key) = extract_key_from_string_key_path(scope, value, path) else { - return Vec::new(); - }; - keys.push(key); - } - return vec![Key::Array(keys)]; - } - let KeyPath::String(key_path) = key_path else { - return Vec::new(); - }; - extract_keys_from_string_key_path(scope, value, key_path, multi_entry) -} - -fn extract_keys_from_string_key_path<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, - key_path: &str, - multi_entry: bool, -) -> Vec { - let Some(current) = value_at_string_key_path(scope, value, key_path) else { - return Vec::new(); - }; - if multi_entry && let Ok(array) = v8::Local::::try_from(current) { + if multi_entry + && let KeyPath::String(path) = key_path + && let Some(value) = value_at_string_key_path(scope, value, path) + && let Ok(array) = v8::Local::::try_from(value) + { let mut keys = Vec::new(); let mut seen = BTreeSet::new(); for index in 0..array.length() { + let property = + v8::String::new(scope, &index.to_string()).expect("array index allocation"); + if array.has_own_property(scope, property.into()) != Some(true) { + continue; + } let Some(entry) = array.get_index(scope, index) else { continue; }; @@ -46,21 +70,10 @@ fn extract_keys_from_string_key_path<'s>( } return keys; } - - parse_idb_key(scope, current) - .ok() - .flatten() - .into_iter() - .collect() -} - -fn extract_key_from_string_key_path<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, - key_path: &str, -) -> Option { - value_at_string_key_path(scope, value, key_path) - .and_then(|value| parse_idb_key(scope, value).ok().flatten()) + match extract_key_from_value(scope, value, key_path) { + ExtractedKey::Key(key) => vec![key], + ExtractedKey::Missing | ExtractedKey::Invalid => Vec::new(), + } } fn value_at_string_key_path<'s>( @@ -73,29 +86,52 @@ fn value_at_string_key_path<'s>( } let mut current = value; for segment in key_path.split('.') { - let Ok(object) = v8::Local::::try_from(current) else { + if segment == "length" + && let Ok(string) = v8::Local::::try_from(current) + { + current = v8::Number::new(scope, string.length() as f64).into(); + continue; + } + let object = v8::Local::::try_from(current).ok()?; + if let Some(special) = special_key_path_property(scope, object, segment) { + current = special; + continue; + } + let property = v8_string(scope, segment)?; + if object.has_own_property(scope, property.into()) != Some(true) { return None; - }; - let next = object.get( - scope, - v8_string(scope, segment) - .unwrap_or_else(|| v8::String::empty(scope)) - .into(), - )?; - current = next; + } + current = object.get(scope, property.into())?; + if current.is_undefined() { + return None; + } } Some(current) } -pub(in crate::context_bootstrap::indexed_db) fn derive_object_store_key_from_value<'s>( +fn special_key_path_property<'s>( scope: &mut v8::PinScope<'s, '_>, - store: v8::Local<'s, v8::Object>, - value: v8::Local<'s, v8::Value>, -) -> Option { - let key_path = store - .get(scope, v8str(scope, "keyPath").into()) - .and_then(|value| key_path_from_js_value(scope, value))?; - extract_index_keys_from_value(scope, value, &key_path, false) - .into_iter() - .next() + object: v8::Local<'s, v8::Object>, + segment: &str, +) -> Option> { + // Read the platform data, not mutable Blob/File prototype accessors. + if matches!(segment, "size" | "type" | "name" | "lastModified") + && let Some(file) = crate::context_bootstrap::selected_file_from_object(scope, object) + { + return Some(match segment { + "size" => v8::Number::new(scope, file.bytes.len() as f64).into(), + "type" => v8_string(scope, &file.mime_type)?.into(), + "name" => v8_string(scope, &file.name)?.into(), + "lastModified" => v8::Number::new(scope, file.last_modified).into(), + _ => unreachable!(), + }); + } + match segment { + "size" => crate::blob::blob_bytes_from_object(scope, object) + .map(|bytes| v8::Number::new(scope, bytes.len() as f64).into()), + "type" => crate::blob::blob_mime_type_from_object(scope, object) + .and_then(|mime| v8_string(scope, &mime)) + .map(Into::into), + _ => None, + } } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/injection.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/injection.rs index 839e94818b..f51bdc3643 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/injection.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/injection.rs @@ -1,76 +1,62 @@ use super::*; -use moli_webapi_declare::ObjectLiteralDeclaration; -pub(super) fn inject_key_path_into_value<'s>( +pub(super) fn can_inject_key<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + key_path: &str, +) -> bool { + if key_path.is_empty() { + return false; + } + let mut current = value; + let mut segments = key_path.split('.').peekable(); + while let Some(segment) = segments.next() { + let Ok(object) = v8::Local::::try_from(current) else { + return false; + }; + if segments.peek().is_none() { + return true; + } + let Some(property) = v8_string(scope, segment) else { + return false; + }; + if object.has_own_property(scope, property.into()) != Some(true) { + return true; + } + let Some(next) = object.get(scope, property.into()) else { + return false; + }; + current = next; + } + false +} + +pub(in crate::context_bootstrap::indexed_db) fn inject_key_path_into_value<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, key_path: &str, key: &Key, -) -> Result, PreparedObjectStoreWriteError> { - let cloned = - clone_js_value(scope, value).ok_or(PreparedObjectStoreWriteError::DomException { - message: "Failed to clone the value before assigning the generated key.", - name: "DataCloneError", - })?; - let mut current = v8::Local::::try_from(cloned).map_err(|_| { - PreparedObjectStoreWriteError::DomException { - message: "Failed to execute the operation: the value cannot accept an inline key.", - name: "DataError", - } - })?; +) -> Option<()> { + // Admission checked injectability on the clone. CreateDataProperty avoids + // inherited setters, including Object.prototype.__proto__. + let mut current = v8::Local::::try_from(value).ok()?; let mut segments = key_path.split('.').peekable(); while let Some(segment) = segments.next() { - let property = v8_string(scope, segment).unwrap_or_else(|| v8::String::empty(scope)); + let property = v8_string(scope, segment)?; if segments.peek().is_none() { - let key_value = key_to_js_value(scope, key); - let _ = current.set(scope, property.into(), key_value); - return Ok(cloned); + let key = key_to_js_value(scope, key); + return (current.create_data_property(scope, property.into(), key) == Some(true)) + .then_some(()); } - let next = current.get(scope, property.into()); - let next_object = match next { - Some(next) if next.is_undefined() => { - let nested = generated_key_path_suffix_object(scope, &mut segments, key); - let _ = current.set(scope, property.into(), nested.into()); - return Ok(cloned); - } - Some(next) => v8::Local::::try_from(next).map_err(|_| { - PreparedObjectStoreWriteError::DomException { - message: - "Failed to execute the operation: the value cannot accept an inline key.", - name: "DataError", - } - })?, - None => { - return Err(PreparedObjectStoreWriteError::DomException { - message: - "Failed to execute the operation: the value cannot accept an inline key.", - name: "DataError", - }); + current = if current.has_own_property(scope, property.into())? { + v8::Local::::try_from(current.get(scope, property.into())?).ok()? + } else { + let nested = v8::Object::new(scope); + if current.create_data_property(scope, property.into(), nested.into()) != Some(true) { + return None; } + nested }; - current = next_object; } - Err(PreparedObjectStoreWriteError::DomException { - message: "Failed to execute the operation: keyPath is empty.", - name: "DataError", - }) -} - -fn generated_key_path_suffix_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - segments: &mut std::iter::Peekable>, - key: &Key, -) -> v8::Local<'s, v8::Object> { - let segment = segments - .next() - .expect("generated key path suffix should include a segment"); - let property = v8_string(scope, segment).unwrap_or_else(|| v8::String::empty(scope)); - let value = if segments.peek().is_none() { - key_to_js_value(scope, key) - } else { - generated_key_path_suffix_object(scope, segments, key).into() - }; - let object = ObjectLiteralDeclaration::bind(scope); - object.set_value_property(scope, property.into(), value); - object.into_object() + None } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/prepare.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/prepare.rs index a61ed50332..bdedbbe3b3 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/prepare.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/prepare.rs @@ -3,70 +3,72 @@ use super::*; pub(in crate::context_bootstrap::indexed_db) fn prepare_object_store_write<'s>( scope: &mut v8::PinScope<'s, '_>, store: v8::Local<'s, v8::Object>, - handle: TransactionHandle, - store_name: &str, + transaction: v8::Local<'s, v8::Object>, value: v8::Local<'s, v8::Value>, - explicit_key: Option, -) -> Result, PreparedObjectStoreWriteError> { - let key_path = store - .get(scope, v8str(scope, "keyPath").into()) - .and_then(|value| key_path_from_js_value(scope, value)); - let auto_increment = object_bool_property(scope, store, "autoIncrement").unwrap_or(false); - if key_path.is_some() && explicit_key.is_some() { - return Err(PreparedObjectStoreWriteError::DomException { - message: "Failed to execute the operation: the store uses in-line keys and does not accept a key argument.", - name: "DataError", - }); + key_value: v8::Local<'s, v8::Value>, +) -> Option { + if indexed_db_transaction_mode(scope, transaction) == Some(TransactionMode::ReadOnly) { + let error = dom_exception_value(scope, "The transaction is readonly.", "ReadOnlyError"); + scope.throw_exception(error); + return None; } - match key_path { - Some(key_path) => { - if let Some(key) = derive_object_store_key_from_value(scope, store, value) { - return Ok(PreparedObjectStoreWrite { - key: Some(key), - value, - }); + let metadata = indexed_db_object_store_metadata(scope, store)?; + let info = metadata.info(); + let has_key = !key_value.is_undefined(); + if (info.key_path.is_some() && has_key) + || (info.key_path.is_none() && !info.auto_increment && !has_key) + { + return invalid_write_key(scope); + } + let mut key = if has_key { + // Explicit array key getters can throw before the record is cloned. + let parsed = { + let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); + let mut scope = try_catch.init(); + let parsed = parse_idb_key(&mut scope, key_value); + if scope.has_caught() { + scope.rethrow(); + return None; } - if !auto_increment { - return Err(PreparedObjectStoreWriteError::DomException { - message: "Failed to execute the operation: the value does not contain the object store keyPath.", - name: "DataError", - }); - } - let generated_key = - with_indexed_db_manager(scope, |manager| manager.generate_key(handle, store_name)) - .map_err(PreparedObjectStoreWriteError::Backend)?; - let KeyPath::String(key_path) = key_path else { - return Err(PreparedObjectStoreWriteError::DomException { - message: "Failed to execute the operation: compound keyPath stores cannot accept generated inline keys.", - name: "InvalidAccessError", - }); - }; - let value = inject_key_path_into_value(scope, value, &key_path, &generated_key)?; - Ok(PreparedObjectStoreWrite { - key: Some(generated_key), - value, - }) + parsed + }; + match parsed { + Ok(Some(key)) => Some(key), + _ => return invalid_write_key(scope), } - None => { - if explicit_key.is_none() && !auto_increment { - return Err(PreparedObjectStoreWriteError::DomException { - message: "Failed to execute the operation: a key is required for stores without autoIncrement.", - name: "DataError", - }); + } else { + None + }; + let (clone, bytes) = clone_value_for_transaction(scope, transaction, value)?; + let mut injection_path = None; + if let Some(key_path) = &info.key_path { + match extract_key_from_value(scope, clone, key_path) { + ExtractedKey::Key(extracted) => key = Some(extracted), + ExtractedKey::Invalid => return invalid_write_key(scope), + ExtractedKey::Missing => { + let KeyPath::String(path) = key_path else { + return invalid_write_key(scope); + }; + if !info.auto_increment || !can_inject_key(scope, clone, path) { + return invalid_write_key(scope); + } + injection_path = Some(path.clone()); } - let key = if let Some(key) = explicit_key { - Some(key) - } else if auto_increment { - Some( - with_indexed_db_manager(scope, |manager| { - manager.generate_key(handle, store_name) - }) - .map_err(PreparedObjectStoreWriteError::Backend)?, - ) - } else { - None - }; - Ok(PreparedObjectStoreWrite { key, value }) } } + Some(PreparedObjectStoreWrite { + key, + value: bytes, + injection_path, + }) +} + +fn invalid_write_key(scope: &mut v8::PinScope<'_, '_>) -> Option { + let error = dom_exception_value( + scope, + "The value or key does not satisfy the object store's key requirements.", + "DataError", + ); + scope.throw_exception(error); + None } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor.rs index 547fbfe328..032f8a4852 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor.rs @@ -1,16 +1,19 @@ use super::{ - CursorDirection, INDEXED_DB_CURSOR_ENTRIES_SLOT, INDEXED_DB_CURSOR_KEY_ONLY_SLOT, + CursorDirection, ExtractedKey, INDEXED_DB_CURSOR_ENTRIES_SLOT, INDEXED_DB_CURSOR_KEY_ONLY_SLOT, INDEXED_DB_CURSOR_POSITION_SLOT, INDEXED_DB_CURSOR_REQUEST_SLOT, INDEXED_DB_INDEX_MARKER_SLOT, - INDEXED_DB_OBJECT_STORE_NAME_SLOT, INDEXED_DB_PENDING_CURSOR_POSITION_SLOT, - INDEXED_DB_PENDING_CURSOR_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT, Key, TransactionHandle, - create_request_object, cursor_direction_from_cursor, cursor_entry_object, - define_public_non_enumerable_value_property, dom_exception_value, - extract_index_keys_from_value, indexed_db_request_transaction_object, key_path_from_js_value, - key_to_js_value, object_bool_property, object_hidden_value, object_number_property, + INDEXED_DB_PENDING_CURSOR_POSITION_SLOT, INDEXED_DB_PENDING_CURSOR_SLOT, + INDEXED_DB_REQUEST_READY_STATE_SLOT, INDEXED_DB_REQUEST_SOURCE_SLOT, + INDEXED_DB_TRANSACTION_ACTIVE_SLOT, Key, PreparedObjectStoreWrite, TransactionMode, + clone_value_for_transaction, create_request_object, cursor_direction_from_cursor, + cursor_entry_object, dom_exception_value, execute_object_store_write_request, + extract_key_from_value, indexed_db_index_is_deleted, indexed_db_index_object_store, + indexed_db_object_store_is_deleted, indexed_db_object_store_metadata, + indexed_db_object_store_name, indexed_db_request_transaction_object, + indexed_db_transaction_mode, object_bool_property, object_hidden_value, object_number_property, object_property_as_object, object_string_property, parse_idb_key, prepare_cursor_request, - queue_transaction_request, request_error_object, serialize_js_value, set_indexed_db_slot_value, - storage_bucket_quota_check_for_object_store, store_request_error, store_request_success, - throw_type_error, transaction_handle_from_value, v8str, with_indexed_db_manager, + queue_transaction_request, request_error_object, set_indexed_db_slot_value, + store_request_error, store_request_success, throw_type_error, transaction_handle_from_value, + v8str, with_indexed_db_manager, }; use crate::webidl; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/mutation/delete.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/mutation/delete.rs index 3c844399b2..ecb0fe403a 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/mutation/delete.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/mutation/delete.rs @@ -6,23 +6,16 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_cursor_delete_callback<'s>( mut rv: v8::ReturnValue<'s, v8::Value>, ) { let cursor = args.this(); - let Some(position) = cursor_current_position(scope, cursor) else { - let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError"); - scope.throw_exception(error); + let Some((store, transaction, primary_key)) = cursor_mutation_state(scope, cursor) else { return; }; - let Some((request, handle, store_name)) = create_cursor_request(scope, cursor) else { - let error = dom_exception_value( - scope, - "The transaction is not active.", - "TransactionInactiveError", - ); - scope.throw_exception(error); + let Some(handle) = transaction_handle_from_value(scope, transaction.into()) else { return; }; - let Some(primary_key) = cursor_primary_key_at(scope, cursor, position) else { - let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError"); - scope.throw_exception(error); + let Some(store_name) = indexed_db_object_store_name(scope, store) else { + return; + }; + let Some(request) = create_request_object(scope, cursor.into(), transaction) else { return; }; match with_indexed_db_manager(scope, |manager| { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/mutation/update.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/mutation/update.rs index 4021f417fa..0e261eda43 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/mutation/update.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/mutation/update.rs @@ -7,44 +7,6 @@ struct IdbCursorUpdateArgs<'s> { value: v8::Local<'s, v8::Value>, } -fn cursor_entry_update_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - cursor: v8::Local<'s, v8::Object>, - position: usize, - value: v8::Local<'s, v8::Value>, -) -> Option<()> { - let entry = cursor_entry_object(scope, cursor, position)?; - define_public_non_enumerable_value_property(scope, entry, "value", value); - if !object_bool_property(scope, cursor, INDEXED_DB_CURSOR_KEY_ONLY_SLOT).unwrap_or(false) { - let _ = cursor.set(scope, v8str(scope, "value").into(), value); - } - Some(()) -} - -fn cursor_update_preserves_primary_key<'s>( - scope: &mut v8::PinScope<'s, '_>, - cursor: v8::Local<'s, v8::Object>, - value: v8::Local<'s, v8::Value>, - primary_key: &Key, -) -> bool { - let Some(store) = cursor_store_object(scope, cursor) else { - return true; - }; - let Some(key_path) = store - .get(scope, v8str(scope, "keyPath").into()) - .and_then(|value| key_path_from_js_value(scope, value)) - else { - return true; - }; - let Some(derived_key) = extract_index_keys_from_value(scope, value, &key_path, false) - .into_iter() - .next() - else { - return false; - }; - &derived_key == primary_key -} - pub(in crate::context_bootstrap::indexed_db) fn idb_cursor_update_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -54,80 +16,55 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_cursor_update_callback<'s>( return; }; let cursor = args.this(); - if object_bool_property(scope, cursor, INDEXED_DB_CURSOR_KEY_ONLY_SLOT).unwrap_or(false) { + let Some((store, transaction, primary_key)) = cursor_mutation_state(scope, cursor) else { + return; + }; + let Some((clone, bytes)) = clone_value_for_transaction(scope, transaction, parsed.value) else { + return; + }; + let Some(metadata) = indexed_db_object_store_metadata(scope, store) else { + return; + }; + if let Some(path) = &metadata.info().key_path + && !matches!(extract_key_from_value(scope, clone, path), ExtractedKey::Key(key) if key == primary_key) + { let error = dom_exception_value( scope, - "The cursor does not expose values.", - "InvalidStateError", - ); - scope.throw_exception(error); - return; - } - let Some(position) = cursor_current_position(scope, cursor) else { - let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError"); - scope.throw_exception(error); - return; - }; - let Some((request, handle, store_name)) = create_cursor_request(scope, cursor) else { - let error = dom_exception_value( - scope, - "The transaction is not active.", - "TransactionInactiveError", - ); - scope.throw_exception(error); - return; - }; - let Some(value_bytes) = serialize_js_value(scope, parsed.value) else { - return; - }; - let Some(primary_key) = cursor_primary_key_at(scope, cursor, position) else { - let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError"); - scope.throw_exception(error); - return; - }; - if !cursor_update_preserves_primary_key(scope, cursor, parsed.value, &primary_key) { - let error = dom_exception_value( - scope, - "Failed to execute 'update': the value changes the effective key.", + "The value changes the cursor's effective key.", "DataError", ); scope.throw_exception(error); return; } - let quota_check = match cursor_store_object(scope, cursor) - .and_then(|store| storage_bucket_quota_check_for_object_store(scope, store)) - { - Some(Ok(quota)) => Some(quota), - Some(Err(error)) => { - let error = request_error_object(scope, &error); - store_request_error(scope, request, error); - rv.set(request.into()); - return; - } - None => None, + let Some(request) = create_request_object(scope, cursor.into(), transaction) else { + return; }; - match with_indexed_db_manager(scope, |manager| { - if let Some(quota) = quota_check { - manager.put_with_quota( - handle, - &store_name, - Some(primary_key.clone()), - value_bytes, - quota.quota_check, - ) - } else { - manager.put(handle, &store_name, Some(primary_key.clone()), value_bytes) - } - }) { - Ok(key) => { - let _ = cursor_entry_update_value(scope, cursor, position, parsed.value); - let key = key_to_js_value(scope, &key); - store_request_success(scope, request, key); - } - Err(error) => { - let error = request_error_object(scope, &error); - store_request_error(scope, request, error); - } - } rv.set(request.into()); + if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ACTIVE_SLOT) + .unwrap_or(false) + { + return; + } + let Some(handle) = transaction_handle_from_value(scope, transaction.into()) else { + return; + }; + let Some(store_name) = indexed_db_object_store_name(scope, store) else { + return; + }; + let prepared = PreparedObjectStoreWrite { + key: Some(primary_key), + value: bytes, + injection_path: None, + }; + // Updating storage does not replace the cursor's cached value. The shared + // executor also applies the store's unique-index and quota constraints. + execute_object_store_write_request( + scope, + store, + request, + handle, + &store_name, + &prepared, + false, + ); } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/state.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/state.rs index 15ffb49cd7..8c93ea78f9 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/state.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/cursor/state.rs @@ -45,25 +45,6 @@ pub(in crate::context_bootstrap::indexed_db) fn cursor_primary_key_at<'s>( parse_idb_key(scope, entry.get(scope, v8str(scope, "primaryKey").into())?).ok()? } -pub(in crate::context_bootstrap::indexed_db) fn cursor_store_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - cursor: v8::Local<'s, v8::Object>, -) -> Option> { - let source = object_property_as_object(scope, cursor, "source")?; - if cursor_source_is_index(scope, cursor) { - return object_property_as_object(scope, source, "objectStore"); - } - Some(source) -} - -pub(in crate::context_bootstrap::indexed_db) fn cursor_store_name<'s>( - scope: &mut v8::PinScope<'s, '_>, - cursor: v8::Local<'s, v8::Object>, -) -> Option { - let store = cursor_store_object(scope, cursor)?; - object_string_property(scope, store, INDEXED_DB_OBJECT_STORE_NAME_SLOT) -} - pub(in crate::context_bootstrap::indexed_db) fn cursor_current_position( scope: &mut v8::PinScope<'_, '_>, cursor: v8::Local<'_, v8::Object>, @@ -72,20 +53,54 @@ pub(in crate::context_bootstrap::indexed_db) fn cursor_current_position( (position >= 0).then_some(position as usize) } -pub(in crate::context_bootstrap::indexed_db) fn create_cursor_request<'s>( +pub(in crate::context_bootstrap::indexed_db) fn cursor_mutation_state<'s>( scope: &mut v8::PinScope<'s, '_>, cursor: v8::Local<'s, v8::Object>, -) -> Option<(v8::Local<'s, v8::Object>, TransactionHandle, String)> { - let (_, transaction) = cursor_request_and_transaction(scope, cursor)?; +) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>, Key)> { + let (request, transaction) = cursor_request_and_transaction(scope, cursor)?; if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ACTIVE_SLOT) .unwrap_or(false) { + let error = dom_exception_value( + scope, + "The transaction is not active.", + "TransactionInactiveError", + ); + scope.throw_exception(error); return None; } - let request = create_request_object(scope, cursor.into(), transaction)?; - let handle = transaction_handle_from_value(scope, transaction.into())?; - let store_name = cursor_store_name(scope, cursor)?; - Some((request, handle, store_name)) + if indexed_db_transaction_mode(scope, transaction) == Some(TransactionMode::ReadOnly) { + let error = dom_exception_value(scope, "The transaction is readonly.", "ReadOnlyError"); + scope.throw_exception(error); + return None; + } + let source = object_hidden_value(scope, request, INDEXED_DB_REQUEST_SOURCE_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let store = indexed_db_index_object_store(scope, source).unwrap_or(source); + if indexed_db_object_store_is_deleted(scope, store) + || (source != store && indexed_db_index_is_deleted(scope, source)) + || object_string_property(scope, request, INDEXED_DB_REQUEST_READY_STATE_SLOT).as_deref() + != Some("done") + || object_bool_property(scope, cursor, INDEXED_DB_CURSOR_KEY_ONLY_SLOT).unwrap_or(false) + { + let error = dom_exception_value( + scope, + "The cursor cannot modify a record in its current state.", + "InvalidStateError", + ); + scope.throw_exception(error); + return None; + } + let Some(position) = cursor_current_position(scope, cursor) else { + let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError"); + scope.throw_exception(error); + return None; + }; + Some(( + store, + transaction, + cursor_primary_key_at(scope, cursor, position)?, + )) } pub(in crate::context_bootstrap::indexed_db) fn cursor_source_is_index<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs index 4a72b4354b..1123f5c773 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs @@ -1,6 +1,5 @@ use super::{ array_contains_strict, array_push_value, context_host_ptr_from_global_bridge, - define_non_enumerable_value_property as define_public_non_enumerable_value_property, global_constructor_prototype, object_bool_property as public_object_bool_property, object_number_property as public_object_number_property, object_property_as_object as public_object_property_as_object, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/operation_state.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/operation_state.rs index 554c24ebad..6b03a820c6 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/operation_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/operation_state.rs @@ -1,4 +1,4 @@ -use super::{CursorDirection, IdbKeyRangeQuery, IndexInfo}; +use super::{CursorDirection, IdbKeyRangeQuery, IndexInfo, PreparedObjectStoreWrite}; use crate::native_bridge::OwnerDispatchScope; pub(super) enum IndexedDbCursorSource { @@ -64,8 +64,7 @@ pub(super) enum IndexedDbTransactionOperationInput<'s> { }, OpenCursor(IndexedDbCursorOpenOperation), ObjectStoreWrite { - value: v8::Local<'s, v8::Value>, - key: v8::Local<'s, v8::Value>, + prepared: PreparedObjectStoreWrite, add_only: bool, }, ObjectStoreDelete { @@ -123,8 +122,7 @@ enum IndexedDbPendingTransactionOperationKind { }, OpenCursor(IndexedDbCursorOpenOperation), ObjectStoreWrite { - value: v8::Global, - key: v8::Global, + prepared: PreparedObjectStoreWrite, add_only: bool, }, ObjectStoreDelete { @@ -181,8 +179,7 @@ pub(super) enum IndexedDbTransactionOperationKindLocals<'s> { }, OpenCursor(IndexedDbCursorOpenOperation), ObjectStoreWrite { - value: v8::Local<'s, v8::Value>, - key: v8::Local<'s, v8::Value>, + prepared: PreparedObjectStoreWrite, add_only: bool, }, ObjectStoreDelete { @@ -251,15 +248,9 @@ impl IndexedDbPendingTransactionOperation { query: v8::Global::new(scope, query), }, Input::OpenCursor(operation) => Pending::OpenCursor(operation), - Input::ObjectStoreWrite { - value, - key, - add_only, - } => Pending::ObjectStoreWrite { - value: v8::Global::new(scope, value), - key: v8::Global::new(scope, key), - add_only, - }, + Input::ObjectStoreWrite { prepared, add_only } => { + Pending::ObjectStoreWrite { prepared, add_only } + } Input::ObjectStoreDelete { key } => Pending::ObjectStoreDelete { key: v8::Global::new(scope, key), }, @@ -348,15 +339,9 @@ impl IndexedDbPendingTransactionOperation { query: v8::Local::new(scope, &query), }, Pending::OpenCursor(operation) => Locals::OpenCursor(operation), - Pending::ObjectStoreWrite { - value, - key, - add_only, - } => Locals::ObjectStoreWrite { - value: v8::Local::new(scope, &value), - key: v8::Local::new(scope, &key), - add_only, - }, + Pending::ObjectStoreWrite { prepared, add_only } => { + Locals::ObjectStoreWrite { prepared, add_only } + } Pending::ObjectStoreDelete { key } => Locals::ObjectStoreDelete { key: v8::Local::new(scope, &key), }, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs index 25e9005f38..d4415faa4f 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs @@ -2,21 +2,21 @@ use super::{ CursorDirection, CursorSnapshotEntry, INDEXED_DB_TRANSACTION_ACTIVE_SLOT, INDEXED_DB_TRANSACTION_STARTED_SLOT, IdbKeyRangeQuery, IndexInfo, IndexOptions, IndexedDbCursorOpenOperation, IndexedDbError, IndexedDbTransactionOperationInput, KeyPath, - PreparedObjectStoreWriteError, TransactionHandle, apply_cursor_direction, - apply_index_collection_direction, apply_object_store_collection_direction, compare_idb_keys, - create_index_object, create_key_range_object, create_request_object, cursor_direction_to_value, - deserialize_js_value, dom_exception_value, enforce_object_store_unique_constraints, - enqueue_transaction_operation, execute_object_store_clear_request, - execute_object_store_delete_request, index_info_from_store_metadata, indexed_db_index_info, + TransactionHandle, apply_cursor_direction, apply_index_collection_direction, + apply_object_store_collection_direction, compare_idb_keys, create_index_object, + create_key_range_object, create_request_object, cursor_direction_to_value, + deserialize_js_value, dom_exception_value, enqueue_transaction_operation, + execute_object_store_clear_request, execute_object_store_delete_request, + execute_object_store_write_request, index_info_from_store_metadata, indexed_db_index_info, indexed_db_index_object_store, indexed_db_object_store_database, indexed_db_object_store_name, indexed_db_object_store_transaction, key_in_range, key_to_js_value, object_bool_property, optional_count_to_value, parse_cursor_direction, parse_cursor_direction_with_context, parse_idb_key, parse_idb_key_path, parse_key_or_range, parse_key_range_from_value, parse_optional_count, prepare_object_store_write, remove_database_index_metadata, - request_error_object, scan_index_entries, scan_object_store_entries, serialize_js_value, - set_database_index_metadata, storage_bucket_quota_check_for_object_store, store_request_error, - store_request_success, submit_cursor_open_operation, throw_type_error, - transaction_handle_from_value, with_indexed_db_manager, + request_error_object, scan_index_entries, scan_object_store_entries, + set_database_index_metadata, store_request_error, store_request_success, + submit_cursor_open_operation, throw_type_error, transaction_handle_from_value, + with_indexed_db_manager, }; use crate::webidl; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/common.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/common.rs index 841b20641a..7412584827 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/common.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/common.rs @@ -1,10 +1,10 @@ use super::*; use crate::context_bootstrap::indexed_db::indexed_db_object_store_is_deleted; -pub(in crate::context_bootstrap::indexed_db) fn create_store_request<'s>( +pub(in crate::context_bootstrap::indexed_db) fn object_store_active_transaction<'s>( scope: &mut v8::PinScope<'s, '_>, source: v8::Local<'s, v8::Object>, -) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> { +) -> Option> { if indexed_db_object_store_is_deleted(scope, source) { let error = dom_exception_value( scope, @@ -26,6 +26,14 @@ pub(in crate::context_bootstrap::indexed_db) fn create_store_request<'s>( scope.throw_exception(error); return None; } + Some(transaction) +} + +pub(in crate::context_bootstrap::indexed_db) fn create_store_request<'s>( + scope: &mut v8::PinScope<'s, '_>, + source: v8::Local<'s, v8::Object>, +) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> { + let transaction = object_store_active_transaction(scope, source)?; let request = create_request_object(scope, source.into(), transaction)?; Some((request, transaction)) } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation.rs index 681c043295..5958c4ddd2 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation.rs @@ -1,8 +1,5 @@ use super::*; -mod enqueue; -mod execute; - pub(super) fn object_store_write_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -12,48 +9,49 @@ pub(super) fn object_store_write_callback<'s>( add_only: bool, ) { let store = args.this(); - let Some((request, transaction)) = create_store_request(scope, store) else { + let Some(transaction) = object_store_active_transaction(scope, store) else { + return; + }; + let Some(prepared) = prepare_object_store_write(scope, store, transaction, value, key) else { return; }; let Some(store_name) = indexed_db_object_store_name(scope, store) else { - rv.set(request.into()); return; }; + // Only accepted operations allocate a request. Both immediate and queued + // writes use the same immutable snapshot and native converted key. + let Some(request) = create_request_object(scope, store.into(), transaction) else { + return; + }; + rv.set(request.into()); + // A getter can abort while the value is being serialized. Admission was + // already checked; return the request without reviving or writing the + // finished transaction. + if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ACTIVE_SLOT) + .unwrap_or(false) + { + return; + } if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_STARTED_SLOT) .unwrap_or(false) { - enqueue::enqueue_deferred_object_store_write( + enqueue_transaction_operation( scope, transaction, store, request, &store_name, - value, - key, + IndexedDbTransactionOperationInput::ObjectStoreWrite { prepared, add_only }, + ); + } else if let Some(handle) = transaction_handle_from_value(scope, transaction.into()) { + execute_object_store_write_request( + scope, + store, + request, + handle, + &store_name, + &prepared, add_only, ); - rv.set(request.into()); - return; - } - let Some(handle) = transaction_handle_from_value(scope, transaction.into()) else { - let error = dom_exception_value( - scope, - "The transaction is not active.", - "TransactionInactiveError", - ); - scope.throw_exception(error); - return; - }; - if execute::execute_started_object_store_write( - scope, - store, - request, - handle, - &store_name, - value, - key, - add_only, - ) { - rv.set(request.into()); } } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation/enqueue.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation/enqueue.rs deleted file mode 100644 index 07ebd3ecff..0000000000 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation/enqueue.rs +++ /dev/null @@ -1,25 +0,0 @@ -use super::*; - -pub(super) fn enqueue_deferred_object_store_write<'s>( - scope: &mut v8::PinScope<'s, '_>, - transaction: v8::Local<'s, v8::Object>, - store: v8::Local<'s, v8::Object>, - request: v8::Local<'s, v8::Object>, - store_name: &str, - value: v8::Local<'s, v8::Value>, - key: v8::Local<'s, v8::Value>, - add_only: bool, -) { - enqueue_transaction_operation( - scope, - transaction, - store, - request, - store_name, - IndexedDbTransactionOperationInput::ObjectStoreWrite { - value, - key, - add_only, - }, - ); -} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation/execute.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation/execute.rs deleted file mode 100644 index d2dd8d7c86..0000000000 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation/execute.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::*; - -pub(super) fn execute_started_object_store_write<'s>( - scope: &mut v8::PinScope<'s, '_>, - store: v8::Local<'s, v8::Object>, - request: v8::Local<'s, v8::Object>, - handle: TransactionHandle, - store_name: &str, - value: v8::Local<'s, v8::Value>, - key_value: v8::Local<'s, v8::Value>, - add_only: bool, -) -> bool { - let explicit_key = match parse_idb_key(scope, key_value) { - Ok(key) => key, - Err(message) => { - throw_type_error(scope, message); - return false; - } - }; - let prepared = - match prepare_object_store_write(scope, store, handle, store_name, value, explicit_key) { - Ok(prepared) => prepared, - Err(PreparedObjectStoreWriteError::DomException { message, name }) => { - let error = dom_exception_value(scope, message, name); - scope.throw_exception(error); - return false; - } - Err(PreparedObjectStoreWriteError::Backend(error)) => { - let error = request_error_object(scope, &error); - store_request_error(scope, request, error); - return true; - } - }; - let Some(value_bytes) = serialize_js_value(scope, prepared.value) else { - return false; - }; - let Some(primary_key) = prepared.key.clone() else { - let error = dom_exception_value( - scope, - "Failed to execute the operation: a key is required for stores without autoIncrement.", - "DataError", - ); - scope.throw_exception(error); - return false; - }; - if let Err(error) = enforce_object_store_unique_constraints( - scope, - store, - handle, - store_name, - &primary_key, - prepared.value, - ) { - let error = request_error_object(scope, &error); - store_request_error(scope, request, error); - return true; - } - let quota_check = match storage_bucket_quota_check_for_object_store(scope, store) { - Some(Ok(quota)) => Some(quota), - Some(Err(error)) => { - let error = request_error_object(scope, &error); - store_request_error(scope, request, error); - return true; - } - None => None, - }; - let result = if add_only { - with_indexed_db_manager(scope, |manager| { - if let Some(quota) = quota_check { - manager.add_with_quota( - handle, - store_name, - prepared.key.clone(), - value_bytes, - quota.quota_check, - ) - } else { - manager.add(handle, store_name, prepared.key.clone(), value_bytes) - } - }) - } else { - with_indexed_db_manager(scope, |manager| { - if let Some(quota) = quota_check { - manager.put_with_quota( - handle, - store_name, - prepared.key.clone(), - value_bytes, - quota.quota_check, - ) - } else { - manager.put(handle, store_name, prepared.key.clone(), value_bytes) - } - }) - }; - match result { - Ok(key) => { - let key_value = key_to_js_value(scope, &key); - store_request_success(scope, request, key_value); - } - Err(error) => { - let error = request_error_object(scope, &error); - store_request_error(scope, request, error); - } - } - true -} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs index 72c7bcbd64..b2a1a97280 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs @@ -12,7 +12,7 @@ use super::{ INDEXED_DB_TRANSACTION_STARTED_SLOT, IndexInfo, IndexedDbCursorOpenOperation, IndexedDbCursorSource, IndexedDbError, IndexedDbPendingTransactionOperation, IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbTransactionOperationInput, - IndexedDbTransactionOperationKindLocals, OpenOptions, PreparedObjectStoreWriteError, + IndexedDbTransactionOperationKindLocals, OpenOptions, PreparedObjectStoreWrite, TransactionHandle, TransactionMode, apply_index_collection_direction, apply_object_store_collection_direction, close_indexed_db_database_connection, create_database_object, create_transaction_object, database_handle_from_value, @@ -27,16 +27,15 @@ use super::{ indexed_db_typed_execution_owner, indexed_db_typed_owner_scope, indexed_db_typed_task_execution_context, indexed_db_typed_task_execution_owner, indexed_db_typed_task_id, indexed_db_typed_task_kind, indexed_db_typed_task_owner_scope, - indexed_db_typed_task_storage_scope, key_to_js_value, + indexed_db_typed_task_storage_scope, inject_key_path_into_value, key_to_js_value, materialize_cursor_result_in_request_realm, object_bool_property, object_hidden_value, object_number_property, object_property_as_object, object_store_cursor_snapshot, object_string_property, parse_idb_key, parse_key_or_range, parse_optional_count, - pop_first_indexed_db_task, prepare_object_store_write, - push_indexed_db_operation_waiting_for_start, push_object_to_indexed_db_runtime_array, - push_unique_object_to_indexed_db_runtime_array, readwrite_transaction_can_start, - refresh_cursor_surface, refresh_database_surface, register_blocked_database_context, - replace_indexed_db_runtime_array, request_error_object, scan_index_entries, - scan_object_store_entries, serialize_js_value, set_indexed_db_slot_value, + pop_first_indexed_db_task, push_indexed_db_operation_waiting_for_start, + push_object_to_indexed_db_runtime_array, push_unique_object_to_indexed_db_runtime_array, + readwrite_transaction_can_start, refresh_cursor_surface, refresh_database_surface, + register_blocked_database_context, replace_indexed_db_runtime_array, request_error_object, + scan_index_entries, scan_object_store_entries, serialize_js_value, set_indexed_db_slot_value, signal_worker_indexed_db_task_wake, storage_bucket_quota_check_for_object_store, storage_bucket_quota_check_for_transaction, take_indexed_db_operations_waiting_for_start, take_indexed_db_task_by_id, transaction_db_key, transaction_handle_from_value, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/object_store/write.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/object_store/write.rs index a8cd6f044f..1b04c07dbd 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/object_store/write.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/object_store/write.rs @@ -5,19 +5,14 @@ pub(super) fn try_dispatch_object_store_write_operation<'s>( operation: &operation::QueuedTransactionOperation<'s>, ) -> bool { match &operation.kind { - IndexedDbTransactionOperationKindLocals::ObjectStoreWrite { - value, - key, - add_only, - } => { + IndexedDbTransactionOperationKindLocals::ObjectStoreWrite { prepared, add_only } => { execute_object_store_write_request( scope, operation.source, operation.request, operation.handle, &operation.store_name, - *value, - *key, + prepared, *add_only, ); } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/object_store/write/put_add.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/object_store/write/put_add.rs index c131498d2a..0a9da4254d 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/object_store/write/put_add.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/object_store/write/put_add.rs @@ -6,43 +6,34 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_object_store_write_reque request: v8::Local<'s, v8::Object>, handle: TransactionHandle, store_name: &str, - value: v8::Local<'s, v8::Value>, - key_value: v8::Local<'s, v8::Value>, + prepared: &PreparedObjectStoreWrite, add_only: bool, ) { - let explicit_key = match parse_idb_key(scope, key_value) { + let primary_key = match prepared.key.clone().map(Ok).unwrap_or_else(|| { + with_indexed_db_manager(scope, |manager| { + manager.next_generated_key(handle, store_name) + }) + }) { Ok(key) => key, - Err(message) => { - let error = dom_exception_value(scope, message, "TypeError"); + Err(error) => { + let error = request_error_object(scope, &error); store_request_error(scope, request, error); return; } }; - let prepared = - match prepare_object_store_write(scope, store, handle, store_name, value, explicit_key) { - Ok(prepared) => prepared, - Err(PreparedObjectStoreWriteError::DomException { message, name }) => { - let error = dom_exception_value(scope, message, name); - store_request_error(scope, request, error); - return; - } - Err(PreparedObjectStoreWriteError::Backend(error)) => { - let error = request_error_object(scope, &error); - store_request_error(scope, request, error); - return; - } - }; - let Some(value_bytes) = serialize_js_value(scope, prepared.value) else { + let Some(value) = deserialize_js_value(scope, &prepared.value) else { return; }; - let Some(primary_key) = prepared.key.clone() else { - let error = dom_exception_value( - scope, - "Failed to execute the operation: a key is required for stores without autoIncrement.", - "DataError", - ); - store_request_error(scope, request, error); - return; + let value_bytes = if let Some(path) = &prepared.injection_path { + if inject_key_path_into_value(scope, value, path, &primary_key).is_none() { + return; + } + let Some(bytes) = serialize_js_value(scope, value) else { + return; + }; + bytes + } else { + prepared.value.clone() }; if let Err(error) = enforce_object_store_unique_constraints( scope, @@ -50,7 +41,7 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_object_store_write_reque handle, store_name, &primary_key, - prepared.value, + value, ) { let error = request_error_object(scope, &error); store_request_error(scope, request, error); @@ -71,12 +62,12 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_object_store_write_reque manager.add_with_quota( handle, store_name, - prepared.key.clone(), + Some(primary_key.clone()), value_bytes, quota.quota_check, ) } else { - manager.add(handle, store_name, prepared.key.clone(), value_bytes) + manager.add(handle, store_name, Some(primary_key.clone()), value_bytes) } }) } else { @@ -85,12 +76,12 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_object_store_write_reque manager.put_with_quota( handle, store_name, - prepared.key.clone(), + Some(primary_key.clone()), value_bytes, quota.quota_check, ) } else { - manager.put(handle, store_name, prepared.key.clone(), value_bytes) + manager.put(handle, store_name, Some(primary_key.clone()), value_bytes) } }) }; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/types.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/types.rs index ae376df459..66a6124314 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/types.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/types.rs @@ -1,4 +1,4 @@ -use super::{IndexedDbError, IndexedDbValue, Key}; +use super::{IndexedDbValue, Key}; #[derive(Debug, Clone)] pub(super) struct IdbKeyRangeQuery { @@ -22,15 +22,9 @@ pub(super) struct CursorSnapshotEntry { pub(super) value: Option, } -pub(super) struct PreparedObjectStoreWrite<'s> { +pub(super) struct PreparedObjectStoreWrite { pub(super) key: Option, - pub(super) value: v8::Local<'s, v8::Value>, -} - -pub(super) enum PreparedObjectStoreWriteError { - Backend(IndexedDbError), - DomException { - message: &'static str, - name: &'static str, - }, + pub(super) value: IndexedDbValue, + // Present only when an auto-generated key must be injected at execution. + pub(super) injection_path: Option, } diff --git a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs index 8900b33f87..aa12be13c0 100644 --- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs @@ -3770,38 +3770,37 @@ fn indexed_db_out_of_line_store_missing_key_throws_data_error() { } #[test] -fn indexed_db_rejects_out_of_range_integer_keys() { - let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-out-of-range-key.test/"); +fn indexed_db_rejects_invalid_explicit_keys() { + let mut vm = + new_storage_page_task_executor_test_vm("https://indexeddb-invalid-explicit-key.test/"); vm.eval( r#" (() => { - globalThis.__indexedDbOutOfRangeKeyError = "pending"; + globalThis.__indexedDbInvalidExplicitKeyError = "pending"; const dbName = `app-${Math.random()}`; const open = indexedDB.open(dbName, 1); open.onupgradeneeded = () => open.result.createObjectStore("outline"); open.onsuccess = () => { - try { - open.result - .transaction("outline", "readwrite") - .objectStore("outline") - .put({ value: 1 }, Number.MAX_SAFE_INTEGER + 1); - globalThis.__indexedDbOutOfRangeKeyError = "no-error"; - } catch (error) { - globalThis.__indexedDbOutOfRangeKeyError = error.name; + const store = open.result.transaction("outline", "readwrite").objectStore("outline"); + const errors = []; + for (const key of [NaN, null, {}, 1n]) { + try { store.put({ value: 1 }, key); errors.push("no-error"); } + catch (error) { errors.push(error.name); } } + globalThis.__indexedDbInvalidExplicitKeyError = errors.join("|"); }; return "scheduled"; })() "#, ) - .expect("indexeddb out-of-range key workflow should schedule"); + .expect("indexeddb invalid-explicit key workflow should schedule"); let result = vm - .eval_after_selected_page_tasks("String(globalThis.__indexedDbOutOfRangeKeyError)") - .expect("indexeddb out-of-range key result should be readable"); + .eval_after_selected_page_tasks("String(globalThis.__indexedDbInvalidExplicitKeyError)") + .expect("indexeddb invalid-explicit key result should be readable"); - assert_eq!(result, "TypeError"); + assert_eq!(result, "DataError|DataError|DataError|DataError"); } #[test] @@ -5316,18 +5315,12 @@ fn indexed_db_cursor_update_and_delete_fail_in_readonly_transactions() { const req = db.transaction("kv").objectStore("kv").openCursor(); req.onsuccess = () => { const cursor = req.result; - const updateReq = cursor.update({ value: 2 }); - updateReq.onerror = event => { - event.preventDefault(); - const deleteReq = cursor.delete(); - deleteReq.onerror = event => { - event.preventDefault(); - globalThis.__indexedDbCursorReadonlyError = [ - updateReq.error && updateReq.error.name, - deleteReq.error && deleteReq.error.name - ].join("|"); - }; - }; + const errors = []; + for (const method of [() => cursor.update({ value: 2 }), () => cursor.delete()]) { + try { method(); errors.push("no-error"); } + catch (error) { errors.push(error.name); } + } + globalThis.__indexedDbCursorReadonlyError = errors.join("|"); }; }; };