fix(indexeddb): snapshot writes before evaluating keys

This commit is contained in:
ldm0
2026-09-23 00:14:08 +08:00
parent c934f8d252
commit e9fad0bb0f
31 changed files with 860 additions and 676 deletions
@@ -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
@@ -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};
}
@@ -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<()> {
+17 -1
View File
@@ -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<Key, IndexedDbError> {
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(),
));
+72
View File
@@ -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();
+16 -6
View File
@@ -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<Key, IndexedDbError> {
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))
}
@@ -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,
@@ -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<IndexedDbValue> {
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
}
@@ -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<KeyPath> {
if value.is_null_or_undefined() {
return None;
}
if let Ok(array) = v8::Local::<v8::Array>::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)))
}
@@ -48,6 +48,11 @@ fn parse_idb_key_with_depth(
if let Ok(array) = v8::Local::<v8::Array>::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.");
};
@@ -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;
@@ -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<v8::Local<'s, v8::Value>> {
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
}
@@ -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<Key> {
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<Key> {
let Some(current) = value_at_string_key_path(scope, value, key_path) else {
return Vec::new();
};
if multi_entry && let Ok(array) = v8::Local::<v8::Array>::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::<v8::Array>::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<Key> {
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::<v8::Object>::try_from(current) else {
if segment == "length"
&& let Ok(string) = v8::Local::<v8::String>::try_from(current)
{
current = v8::Number::new(scope, string.length() as f64).into();
continue;
}
let object = v8::Local::<v8::Object>::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<Key> {
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<v8::Local<'s, v8::Value>> {
// 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,
}
}
@@ -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::<v8::Object>::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<v8::Local<'s, v8::Value>, 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::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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<std::str::Split<'_, char>>,
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
}
@@ -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<Key>,
) -> Result<PreparedObjectStoreWrite<'s>, 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<PreparedObjectStoreWrite> {
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<T>(scope: &mut v8::PinScope<'_, '_>) -> Option<T> {
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
}
@@ -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;
@@ -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| {
@@ -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,
);
}
@@ -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<v8::Local<'s, v8::Object>> {
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<String> {
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::<v8::Object>::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>(
@@ -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,
@@ -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<v8::Value>,
key: v8::Global<v8::Value>,
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),
},
@@ -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;
@@ -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<v8::Local<'s, v8::Object>> {
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))
}
@@ -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());
}
}
@@ -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,
},
);
}
@@ -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
}
@@ -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,
@@ -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,
);
}
@@ -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)
}
})
};
@@ -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<IndexedDbValue>,
}
pub(super) struct PreparedObjectStoreWrite<'s> {
pub(super) struct PreparedObjectStoreWrite {
pub(super) key: Option<Key>,
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<String>,
}
@@ -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("|");
};
};
};