diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index ca647b7e37..99a9267573 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -75,6 +75,8 @@ IndexedDB/idbindex_getAllRecords.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbindex_getAllRecords.any.js?moli-wpt-any=window IndexedDB/idbkeyrange-includes.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbkeyrange-includes.any.js?moli-wpt-any=window +IndexedDB/idbkeyrange.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbkeyrange.any.js?moli-wpt-any=window 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 diff --git a/moli-core/tests/web_apis/fixtures/indexeddb-key-range.js b/moli-core/tests/web_apis/fixtures/indexeddb-key-range.js new file mode 100644 index 0000000000..649109c468 --- /dev/null +++ b/moli-core/tests/web_apis/fixtures/indexeddb-key-range.js @@ -0,0 +1,211 @@ +globalThis.rangeChecks = []; +async function keyRangeProbe(name = 'key-range-' + Math.random()) { + const checks = globalThis.rangeChecks; + const check = (label, pass, actual = '') => checks.push({label, pass: !!pass, actual: JSON.stringify(actual)}); + const equal = (label, actual, expected) => check(label, JSON.stringify(actual) === JSON.stringify(expected), actual); + const throws = (label, call, name, identity) => { + try { call(); check(label, false, 'accepted'); } + catch (error) { check(label, error.name === name && (!identity || error === identity), error.name); } + }; + const request = r => new Promise((resolve, reject) => { + r.onsuccess = () => resolve(r.result); + r.onerror = () => reject(r.error); + }); + const done = tx => new Promise((resolve, reject) => { + tx.oncomplete = resolve; + tx.onabort = () => reject(tx.error); + }); + const includes = IDBKeyRange.prototype.includes; + const names = ['lower', 'upper', 'lowerOpen', 'upperOpen']; + const descriptors = Object.fromEntries(names.map(name => [name, Object.getOwnPropertyDescriptor(IDBKeyRange.prototype, name)])); + for (const [method, length] of [['only',1], ['bound',2], ['lowerBound',1], ['upperBound',1]]) { + equal(method + ' arity', IDBKeyRange[method].length, length); + throws(method + ' missing key', () => IDBKeyRange[method](), 'TypeError'); + } + throws('bound missing upper', () => IDBKeyRange.bound(1), 'TypeError'); + throws('illegal constructor', () => new IDBKeyRange(), 'TypeError'); + equal('static method has no receiver requirement', IDBKeyRange.only.call(null, 3).lower, 3); + for (const [label, range, expected, membership] of [ + ['only', IDBKeyRange.only(2), [2,2,false,false], [false,false,true,false,false]], + ['lower', IDBKeyRange.lowerBound(2), [2,undefined,false,true], [false,false,true,true,true]], + ['lower open', IDBKeyRange.lowerBound(2,true), [2,undefined,true,true], [false,false,false,true,true]], + ['upper', IDBKeyRange.upperBound(2), [undefined,2,true,false], [true,true,true,false,false]], + ['upper open', IDBKeyRange.upperBound(2,true), [undefined,2,true,true], [true,true,false,false,false]], + ['bound', IDBKeyRange.bound(1,3,true,false), [1,3,true,false], [false,false,true,true,false]] + ]) { + equal(label + ' bounds', names.map(name => range[name]), expected); + equal(label + ' membership', [0,1,2,3,4].map(key => includes.call(range,key)), membership); + } + const real = IDBKeyRange.only(2); + equal('no own range attributes or markers', Reflect.ownKeys(real), []); + for (const name of names) { + const descriptor = descriptors[name]; + check('readonly prototype accessor ' + name, descriptor && typeof descriptor.get === 'function' && descriptor.set === undefined && descriptor.enumerable && descriptor.configurable); + check('ordinary write rejected ' + name, Reflect.set(real,name,123) === false); + throws('strict write rejected ' + name, () => { 'use strict'; real[name] = 123; }, 'TypeError'); + if (descriptor && descriptor.get) equal('getter arity ' + name, descriptor.get.length, 0); + } + const revoked = Proxy.revocable(real,{}); revoked.revoke(); + for (const [label, receiver] of [ + ['plain',{}], ['prototype',IDBKeyRange.prototype], + ['forged',Object.create(IDBKeyRange.prototype)], ['inheritor',Object.create(real)], + ['proxy',new Proxy(real,{})], ['revoked',revoked.proxy], ['null',null] + ]) { + let reads = 0; + const key = Object.defineProperty([],0,{get(){reads++;throw new Error('key read');}}); + throws('includes brand ' + label, () => includes.call(receiver,key), 'TypeError'); + equal('brand before key conversion ' + label, reads, 0); + for (const name of names) { + const getter = descriptors[name] && descriptors[name].get; + if (getter) throws('getter brand ' + label + ' ' + name, () => getter.call(receiver), 'TypeError'); + } + } + const order = [], sentinel = new URIError('key conversion'); + const lower = Object.defineProperty([],0,{get(){order.push('lower');return 1;}}); + const upper = Object.defineProperty([],0,{get(){order.push('upper');return 3;}}); + const open = {valueOf(){throw new Error('boolean conversion called valueOf');}}; + const converted = IDBKeyRange.bound(lower,upper,open); + equal('convert keys once in order', order, ['lower','upper']); + equal('boolean conversion', [converted.lowerOpen,converted.upperOpen], [true,false]); + const bad = Object.defineProperty([],0,{get(){throw sentinel;}}); + throws('includes preserves getter exception', () => includes.call(real,bad), 'URIError', sentinel); + throws('bound preserves getter exception', () => IDBKeyRange.bound(bad,upper), 'URIError', sentinel); + equal('failed lower skips upper conversion', order, ['lower','upper']); + throws('includes missing key', () => includes.call(real), 'TypeError'); + for (const invalid of [undefined,null,NaN,new Date(NaN),[undefined],{},new Proxy(real,{})]) { + throws('includes invalid key', () => includes.call(real,invalid), 'DataError'); + } + for (const [label, make, mutate] of [ + ['array', () => [[1]], value => value[0][0] = 99], + ['date', () => new Date(1), value => value.setTime(99)], + ['binary', () => new Uint8Array([1]).buffer, value => new Uint8Array(value)[0] = 99] + ]) { + const input = make(), range = IDBKeyRange.only(input); + mutate(input); + check(label + ' captures input', includes.call(range,make())); + check(label + ' rejects changed input', !includes.call(range,input)); + for (const name of ['lower','upper']) { + const value = range[name]; mutate(value); + check(label + ' ' + name + ' cannot change native range', includes.call(range,make())); + check(label + ' ' + name + ' cannot add a key', !includes.call(range,value)); + } + } + const bare = IDBKeyRange.only(2); Object.setPrototypeOf(bare,null); Object.freeze(bare); + check('brand survives prototype removal and freeze', includes.call(bare,2)); + for (const name of names) { + const getter = descriptors[name] && descriptors[name].get; + if (getter) equal('getter survives prototype removal ' + name, getter.call(bare), name.endsWith('Open') ? false : 2); + } + // Shadow every public attribute with a throwing getter. Queries must use + // the native bounds even when no IDBKeyRange prototype remains reachable. + let publicReads = 0; + const poison = (range = IDBKeyRange.bound([1],[3],true,false)) => { + const lower = range.lower, upper = range.upper; + if (lower !== undefined) lower[0] = 90; + if (upper !== undefined) upper[0] = 99; + for (const name of names) Object.defineProperty(range,name,{get(){publicReads++;throw new Error('public range attribute read');}}); + for (const name of ['__moli_idb_key_range_lower','__moli_idb_key_range_upper','__moli_idb_key_range_lower_open','__moli_idb_key_range_upper_open','__moliIndexedDbKeyRangeMarker']) { + Object.defineProperty(range,name,{value:null}); + } + Object.setPrototypeOf(range,null); Object.freeze(range); + return range; + }; + const verifyQueries = (store, label) => { + const pending = []; + for (const [rangeName, range, ids] of [ + ['bounded',poison(),[2,3]], + ['lower',poison(IDBKeyRange.lowerBound([2],true)),[3,4]], + ['upper',poison(IDBKeyRange.upperBound([3],true)),[1,2]], + ['empty',poison(IDBKeyRange.lowerBound([9])),[]] + ]) for (const [sourceName, source] of [['store',store],['index',store.index('i')]]) { + for (const method of ['get','getKey','getAll','getAllKeys','getAllRecords','count','openCursor','openKeyCursor']) { + const test = label + ' ' + rangeName + ' ' + sourceName + '.' + method; + try { + const r = source[method](method === 'getAllRecords' ? {query:range} : range); + pending.push(request(r).then(value => { + let actual, expected; + if (method === 'get') { actual = value && value.id; expected = ids[0]; } + else if (method === 'getKey') { actual = value; expected = ids.length ? [ids[0]] : undefined; } + else if (method === 'getAll') { actual = value.map(v => v.id); expected = ids; } + else if (method === 'getAllKeys') { actual = value; expected = ids.map(id => [id]); } + else if (method === 'getAllRecords') { actual = value.map(v => v.primaryKey); expected = ids.map(id => [id]); } + else if (method === 'count') { actual = value; expected = ids.length; } + else { actual = value && [value.key,value.primaryKey]; expected = ids.length ? [[ids[0]],[ids[0]]] : null; } + equal(test,actual,expected); + })); + } catch (error) { check(test,false,error.name); } + } + } + return Promise.all(pending); + }; + const eager = [], opening = indexedDB.open(name,1); + opening.onupgradeneeded = () => { + const db = opening.result; + for (const name of ['s','d']) { + const store = db.createObjectStore(name); store.createIndex('i','group'); + for (let id=1;id<=4;id++) store.put({id,group:[id]},[id]); + } + eager.push(verifyQueries(opening.transaction.objectStore('s'),'upgrade')); + }; + const db = await request(opening); + try { + await Promise.all(eager); + for (const mode of ['pending','started']) { + const writer = db.transaction('s','readwrite'), written = done(writer); + writer.objectStore('s').get([0]); + const tx = db.transaction('s'), complete = done(tx), store = tx.objectStore('s'); + if (mode === 'started') await request(store.get([0])); + await verifyQueries(store,mode); + await complete; await written; + const blocker = db.transaction('d','readwrite'), unblocked = done(blocker); + blocker.objectStore('d').get([0]); + const deletion = db.transaction('d','readwrite'), deleted = done(deletion), deletes = deletion.objectStore('d'); + if (mode === 'started') await request(deletes.get([0])); + // Reseed inside the transaction to cover both immediate and queued delete. + for (let id=1;id<=4;id++) deletes.put({id,group:[id]},[id]); + try { await request(deletes.delete(poison())); } + catch (error) { check(mode + ' delete accepted',false,error.name); } + equal(mode + ' delete respects private bounds', await request(deletes.getAllKeys()), [[1],[4]]); + equal(mode + ' delete updates index', await request(deletes.index('i').getAllKeys()), [[1],[4]]); + await deleted; await unblocked; + } + // Prototype pollution must not become query input either. + const polluted = IDBKeyRange.only([2]); + let result; + try { + for (const name of names) Object.defineProperty(IDBKeyRange.prototype,name,{configurable:true,get(){publicReads++;throw new Error('prototype range attribute read');}}); + try { check('includes ignores prototype attributes',includes.call(polluted,[2])); } + catch (error) { check('includes ignores prototype attributes',false,error.name); } + result = request(db.transaction('s').objectStore('s').getAllKeys(polluted)); + } finally { + for (const name of names) { + if (descriptors[name]) Object.defineProperty(IDBKeyRange.prototype,name,descriptors[name]); + else delete IDBKeyRange.prototype[name]; + } + } + equal('query ignores prototype attributes',await result,[[2]]); + equal('queries never invoke public attributes',publicReads,0); + if (typeof document !== 'undefined') { + const frame = document.createElement('iframe'); document.body.appendChild(frame); + const child = frame.contentWindow; + try { + const foreign = child.IDBKeyRange.only([2]); + check('includes accepts foreign range',includes.call(foreign,[2])); + check('foreign includes accepts local range',child.IDBKeyRange.prototype.includes.call(real,2)); + equal('query accepts foreign range',await request(db.transaction('s').objectStore('s').getAllKeys(foreign)),[[2]]); + for (const name of names) { + const descriptor = Object.getOwnPropertyDescriptor(child.IDBKeyRange.prototype,name); + if (!descriptor || !descriptor.get) { check('foreign getter ' + name,false); continue; } + const value = descriptor.get.call(IDBKeyRange.only([2])); + check('foreign getter accepts local range ' + name, name.endsWith('Open') ? value === false : value instanceof child.Array && value[0] === 2); + try { descriptor.get.call(new Proxy(real,{})); check('getter TypeError callee realm ' + name,false); } + catch (error) { check('getter TypeError callee realm ' + name,error instanceof child.TypeError && !(error instanceof TypeError)); } + } + try { child.IDBKeyRange.prototype.includes.call({},2); check('includes TypeError callee realm',false); } + catch (error) { check('includes TypeError callee realm',error instanceof child.TypeError && !(error instanceof TypeError)); } + } finally { frame.remove(); } + } + } finally { db.close(); } + await request(indexedDB.deleteDatabase(name)); + return {state:checks.every(check => check.pass) ? 'pass' : 'fail',checks}; +} diff --git a/moli-core/tests/web_apis/indexed_db_transaction.rs b/moli-core/tests/web_apis/indexed_db_transaction.rs index e777bcc1a3..b092d80aeb 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_key_ranges_protect_bounds_and_validate_receivers() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let fixture = include_str!("fixtures/indexeddb-key-range.js"); + for target in ["window", "child", "worker"] { + let source = format!( + "{fixture}\nkeyRangeProbe('key-range-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: rangeChecks}}));" + ); + let result = run_probe(&browser, &server, target, &source).await?; + assert_eq!(result["state"], "pass", "{target}: {result}"); + assert_eq!( + result["checks"].as_array().unwrap().len(), + if target == "worker" { 317 } else { 329 }, + "{target}: {result}" + ); + } + server.shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn indexed_db_get_all_records_preserves_snapshots_options_and_realms() -> Result<()> { let server = FixtureServer::spawn().await?; 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 b2ce5162c4..4a5e6bd31a 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs @@ -3,14 +3,13 @@ use super::{ INDEXED_DB_CURSOR_ENTRIES_SLOT, INDEXED_DB_CURSOR_KEY_ONLY_SLOT, INDEXED_DB_CURSOR_POSITION_SLOT, INDEXED_DB_DATABASE_CLOSED_SLOT, 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_ABORTED_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT, - INDEXED_DB_TRANSACTION_COMMITTING_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, + INDEXED_DB_REQUEST_ERROR_SLOT, INDEXED_DB_REQUEST_READY_STATE_SLOT, + INDEXED_DB_REQUEST_RESULT_SLOT, INDEXED_DB_TRANSACTION_ABORTED_SLOT, + INDEXED_DB_TRANSACTION_ACTIVE_SLOT, INDEXED_DB_TRANSACTION_COMMITTING_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, @@ -18,9 +17,8 @@ use super::{ object_bool_property, object_hidden_value, object_number_property, object_property_as_object, object_string_property, push_unique_object_to_indexed_db_runtime_array, register_indexed_db_cursor_lifecycle, register_indexed_db_database_lifecycle, - register_indexed_db_index_lifecycle, register_indexed_db_key_range_lifecycle, - register_indexed_db_object_store_lifecycle, register_indexed_db_request_lifecycle, - register_indexed_db_transaction_lifecycle, register_indexed_db_wrapper, + register_indexed_db_index_lifecycle, register_indexed_db_object_store_lifecycle, + register_indexed_db_request_lifecycle, register_indexed_db_transaction_lifecycle, register_indexed_db_wrapper_with_owner, remove_indexed_db_database_index_metadata, remove_indexed_db_database_store_metadata, replace_indexed_db_database_metadata, replace_indexed_db_runtime_array, set_indexed_db_database_index_metadata, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range.rs index dffeecf74f..4dcd794bb3 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range.rs @@ -1,9 +1,19 @@ use super::*; +mod accessors; mod object; mod parse; mod predicate; +const LOWER: &str = "__moli_idb_key_range_lower"; +const UPPER: &str = "__moli_idb_key_range_upper"; +const LOWER_OPEN: &str = "__moli_idb_key_range_lower_open"; +const UPPER_OPEN: &str = "__moli_idb_key_range_upper_open"; + +pub(in crate::context_bootstrap::indexed_db) use self::accessors::{ + idb_key_range_lower_getter, idb_key_range_lower_open_getter, idb_key_range_upper_getter, + idb_key_range_upper_open_getter, +}; pub(in crate::context_bootstrap::indexed_db) use self::object::create_key_range_object; pub(in crate::context_bootstrap::indexed_db) use self::parse::{ parse_key_or_range, parse_key_range_from_value, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/accessors.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/accessors.rs new file mode 100644 index 0000000000..84e322f7c0 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/accessors.rs @@ -0,0 +1,53 @@ +use super::*; +use crate::util::get_private_value; + +pub(in crate::context_bootstrap::indexed_db) fn idb_key_range_lower_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'s, v8::Value>, +) { + bound_getter(scope, args.this(), LOWER, rv); +} + +pub(in crate::context_bootstrap::indexed_db) fn idb_key_range_upper_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'s, v8::Value>, +) { + bound_getter(scope, args.this(), UPPER, rv); +} + +fn bound_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + range: v8::Local<'s, v8::Object>, + slot: &str, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + // Private keys contain only native dense arrays, dates and fixed buffers. + // Reconstruct a value in the getter's realm without exposing the snapshot. + if let Some(value) = get_private_value(scope, range, slot) + && let Ok(Some(key)) = parse_idb_key(scope, value) + { + rv.set(key_to_js_value(scope, &key)); + } +} + +pub(in crate::context_bootstrap::indexed_db) fn idb_key_range_lower_open_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + if let Some(value) = get_private_value(scope, args.this(), LOWER_OPEN) { + rv.set(value); + } +} + +pub(in crate::context_bootstrap::indexed_db) fn idb_key_range_upper_open_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + if let Some(value) = get_private_value(scope, args.this(), UPPER_OPEN) { + rv.set(value); + } +} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/object.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/object.rs index e75325e6e8..bf3388550d 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/object.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/object.rs @@ -3,16 +3,15 @@ use crate::web_api_interfaces; use moli_webapi_declare::WebApiObject; #[derive(WebApiObject)] -#[webapi( - interface = web_api_interfaces::IDBKeyRange, - require_prototype, - data_properties, - enumerable -)] +#[webapi(interface = web_api_interfaces::IDBKeyRange, require_prototype)] struct IdbKeyRangeObjectDeclaration<'scope> { + #[webapi(slot = LOWER)] lower: v8::Local<'scope, v8::Value>, + #[webapi(slot = UPPER)] upper: v8::Local<'scope, v8::Value>, + #[webapi(slot = LOWER_OPEN)] lower_open: bool, + #[webapi(slot = UPPER_OPEN)] upper_open: bool, } @@ -30,11 +29,9 @@ pub(in crate::context_bootstrap::indexed_db) fn create_key_range_object<'s>( .as_ref() .map(|key| key_to_js_value(scope, key)) .unwrap_or_else(|| v8::undefined(scope).into()); - let key_range = - IdbKeyRangeObjectDeclaration::new(lower, upper, range.lower_open, range.upper_open) - .bind(scope) - .ok()?; - register_indexed_db_wrapper(scope, key_range, IndexedDbWrapperKind::KeyRange, None); - register_indexed_db_key_range_lifecycle(scope, key_range, true); - Some(key_range) + // The range owns private snapshots that are never exposed to author code. + // V8 traces them with the range, without a runtime table of strong roots. + IdbKeyRangeObjectDeclaration::new(lower, upper, range.lower_open, range.upper_open) + .bind(scope) + .ok() } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/parse.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/parse.rs index ae274b6d27..d9b867650a 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/parse.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/range/parse.rs @@ -1,18 +1,25 @@ use super::*; +use crate::{util::get_private_value, web_api_interfaces}; pub(in crate::context_bootstrap::indexed_db) fn parse_key_range_from_value<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, ) -> Option { let object = v8::Local::::try_from(value).ok()?; - if !object_bool_property(scope, object, INDEXED_DB_KEY_RANGE_MARKER_SLOT).unwrap_or(false) { + if !web_api_interfaces::IDBKeyRange::is_instance(scope, object) { return None; } + // get_private_value maps undefined to None; an undefined bound is valid + // and denotes the unbounded end of lowerBound()/upperBound(). + let lower = + get_private_value(scope, object, LOWER).unwrap_or_else(|| v8::undefined(scope).into()); + let upper = + get_private_value(scope, object, UPPER).unwrap_or_else(|| v8::undefined(scope).into()); Some(IdbKeyRangeQuery { - lower: parse_idb_key(scope, object.get(scope, v8str(scope, "lower").into())?).ok()?, - upper: parse_idb_key(scope, object.get(scope, v8str(scope, "upper").into())?).ok()?, - lower_open: object_bool_property(scope, object, "lowerOpen").unwrap_or(false), - upper_open: object_bool_property(scope, object, "upperOpen").unwrap_or(false), + lower: parse_idb_key(scope, lower).ok()?, + upper: parse_idb_key(scope, upper).ok()?, + lower_open: get_private_value(scope, object, LOWER_OPEN)?.is_true(), + upper_open: get_private_value(scope, object, UPPER_OPEN)?.is_true(), }) } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/install/constructors/cursor_key_range.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/install/constructors/cursor_key_range.rs index bbd8c19065..b21683490e 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/install/constructors/cursor_key_range.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/install/constructors/cursor_key_range.rs @@ -1,4 +1,8 @@ use super::*; +use crate::context_bootstrap::indexed_db::{ + idb_key_range_lower_getter, idb_key_range_lower_open_getter, idb_key_range_upper_getter, + idb_key_range_upper_open_getter, +}; use crate::web_api_interfaces; use moli_webapi_declare::WebApiFunctionTemplate; @@ -22,8 +26,16 @@ struct IdbCursorPrototypeDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::IDBKeyRange, enumerable)] +#[webapi(interface = web_api_interfaces::IDBKeyRange, enumerable, receiver)] struct IdbKeyRangePrototypeDeclaration { + #[webapi(accessor_property, getter = idb_key_range_lower_getter)] + lower: (), + #[webapi(accessor_property, getter = idb_key_range_upper_getter)] + upper: (), + #[webapi(accessor_property, getter = idb_key_range_lower_open_getter)] + lower_open: (), + #[webapi(accessor_property, getter = idb_key_range_upper_open_getter)] + upper_open: (), #[webapi(method, length = 1, callback = idb_key_range_includes_callback)] includes: (), } @@ -33,17 +45,17 @@ struct IdbKeyRangePrototypeDeclaration { struct IdbKeyRangeConstructorDeclaration { #[webapi(static_method, length = 1, callback = idb_key_range_only_callback)] only: (), - #[webapi(static_method, length = 4, callback = idb_key_range_bound_callback)] + #[webapi(static_method, length = 2, callback = idb_key_range_bound_callback)] bound: (), #[webapi( static_method, - length = 2, + length = 1, callback = idb_key_range_lower_bound_callback )] lower_bound: (), #[webapi( static_method, - length = 2, + length = 1, callback = idb_key_range_upper_bound_callback )] upper_bound: (), diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/slots.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/slots.rs index fe0c436d73..43f9472d01 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/slots.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/slots.rs @@ -38,7 +38,6 @@ pub(super) const INDEXED_DB_TRANSACTION_ABORT_DISPATCHED_SLOT: &str = pub(super) const INDEXED_DB_OBJECT_STORE_NAME_SLOT: &str = "moli.IndexedDb.objectStoreName"; pub(super) const INDEXED_DB_OBJECT_STORE_METADATA_SLOT: &str = "moli.IndexedDb.objectStoreMetadata"; pub(super) const INDEXED_DB_INDEX_MARKER_SLOT: &str = "moli.IndexedDb.indexMarker"; -pub(super) const INDEXED_DB_KEY_RANGE_MARKER_SLOT: &str = "moli.IndexedDb.keyRangeMarker"; pub(super) const INDEXED_DB_CURSOR_REQUEST_SLOT: &str = "moli.IndexedDb.cursorRequest"; pub(super) const INDEXED_DB_CURSOR_ENTRIES_SLOT: &str = "moli.IndexedDb.cursorEntries"; pub(super) const INDEXED_DB_CURSOR_POSITION_SLOT: &str = "moli.IndexedDb.cursorPosition"; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/constructors/lower_bound.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/constructors/lower_bound.rs index 3296ed4104..1457bcea24 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/constructors/lower_bound.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/constructors/lower_bound.rs @@ -24,7 +24,7 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_key_range_lower_bound_callba lower: Some(lower), upper: None, lower_open: parsed.open, - upper_open: false, + upper_open: true, }; if let Some(object) = create_key_range_object(scope, &range) { rv.set(object.into()); diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/constructors/upper_bound.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/constructors/upper_bound.rs index 7c29d92050..2e38f48e09 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/constructors/upper_bound.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/constructors/upper_bound.rs @@ -23,7 +23,7 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_key_range_upper_bound_callba let range = IdbKeyRangeQuery { lower: None, upper: Some(upper), - lower_open: false, + lower_open: true, upper_open: parsed.open, }; if let Some(object) = create_key_range_object(scope, &range) { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/instance.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/instance.rs index 56a9c86a7c..1b4dfcd34c 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/instance.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/key_range/instance.rs @@ -16,7 +16,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_key_range_includes_callback< return; }; let Some(range) = parse_key_range_from_value(scope, args.this().into()) else { - rv.set_bool(false); return; }; let Some(key) = require_idb_key(scope, parsed.key) else { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs index 72d86bf5cb..8a827ee796 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs @@ -32,7 +32,6 @@ pub(super) enum IndexedDbWrapperKind { Cursor, ObjectStore, Index, - KeyRange, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -165,8 +164,7 @@ impl IndexedDbWrapperState { | IndexedDbWrapperKind::Transaction | IndexedDbWrapperKind::Cursor | IndexedDbWrapperKind::ObjectStore - | IndexedDbWrapperKind::Index - | IndexedDbWrapperKind::KeyRange => self.owner.dispatch_scope(), + | IndexedDbWrapperKind::Index => self.owner.dispatch_scope(), } } } @@ -606,10 +604,6 @@ impl IndexedDbIndexLifecycleState { } } -struct IndexedDbKeyRangeLifecycleState { - marker: bool, -} - #[derive(Default)] pub(super) struct IndexedDbRuntimeStateTable { next_id: u64, @@ -628,7 +622,6 @@ pub(super) struct IndexedDbRuntimeStateTable { cursors: BTreeMap, object_stores: BTreeMap, indexes: BTreeMap, - key_ranges: BTreeMap, } impl IndexedDbRuntimeStateTable { @@ -1138,21 +1131,6 @@ pub(super) fn register_indexed_db_index_lifecycle<'s>( ); } -pub(super) fn register_indexed_db_key_range_lifecycle<'s>( - scope: &mut v8::PinScope<'s, '_>, - key_range: v8::Local<'s, v8::Object>, - marker: bool, -) { - let Some(id) = indexed_db_typed_state_id(scope, key_range) else { - return; - }; - let table = indexed_db_runtime_state_table_for_object(scope, key_range); - table - .borrow_mut() - .key_ranges - .insert(id, IndexedDbKeyRangeLifecycleState { marker }); -} - pub(super) fn indexed_db_typed_owner_scope<'s>( scope: &mut v8::PinScope<'s, '_>, wrapper: v8::Local<'s, v8::Object>, @@ -2050,9 +2028,6 @@ pub(super) fn indexed_db_typed_slot_value<'s>( if let Some(index) = table.indexes.get(&id) { return indexed_db_typed_index_slot_value(scope, index, key); } - if let Some(key_range) = table.key_ranges.get(&id) { - return indexed_db_typed_key_range_slot_value(scope, key_range, key); - } None } @@ -2085,9 +2060,6 @@ pub(super) fn set_indexed_db_typed_slot_value( if let Some(index) = table.indexes.get_mut(&id) { return set_indexed_db_typed_index_slot_value(index, key, value); } - if let Some(key_range) = table.key_ranges.get_mut(&id) { - return set_indexed_db_typed_key_range_slot_value(key_range, key, value); - } false } @@ -2479,31 +2451,6 @@ fn set_indexed_db_typed_index_slot_value( } } -fn indexed_db_typed_key_range_slot_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - key_range: &IndexedDbKeyRangeLifecycleState, - key: &str, -) -> Option> { - match key { - INDEXED_DB_KEY_RANGE_MARKER_SLOT => Some(v8::Boolean::new(scope, key_range.marker).into()), - _ => None, - } -} - -fn set_indexed_db_typed_key_range_slot_value( - key_range: &mut IndexedDbKeyRangeLifecycleState, - key: &str, - value: v8::Local<'_, v8::Value>, -) -> bool { - match key { - INDEXED_DB_KEY_RANGE_MARKER_SLOT => { - key_range.marker = value.is_true(); - true - } - _ => false, - } -} - fn indexed_db_typed_state_id( scope: &mut v8::PinScope<'_, '_>, wrapper: v8::Local<'_, v8::Object>, 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 1dc32974d9..55b05ed6c9 100644 --- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs @@ -4037,10 +4037,8 @@ fn indexed_db_index_queries_and_key_ranges_work() { .eval_after_selected_page_tasks("String(globalThis.__indexedDbIndexQueryResult)") .expect("indexeddb index query result should be readable"); - assert_eq!( - result, - "lower,lowerOpen,upper,upperOpen|true|news|news|false|false|a|a|a,c|a,c|2" - ); + // Range attributes are inherited readonly accessors, not own properties. + assert_eq!(result, "|true|news|news|false|false|a|a|a,c|a,c|2"); } #[test] diff --git a/moli-renderer-v8/src/script_vm/tests/post_parse.rs b/moli-renderer-v8/src/script_vm/tests/post_parse.rs index 09a192f8e8..e13e01d681 100644 --- a/moli-renderer-v8/src/script_vm/tests/post_parse.rs +++ b/moli-renderer-v8/src/script_vm/tests/post_parse.rs @@ -4109,9 +4109,9 @@ fn indexed_db_declared_methods_have_webidl_operation_descriptors() { [IDBCursor.prototype, "delete", 0], [IDBKeyRange.prototype, "includes", 1], [IDBKeyRange, "only", 1], - [IDBKeyRange, "bound", 4], - [IDBKeyRange, "lowerBound", 2], - [IDBKeyRange, "upperBound", 2], + [IDBKeyRange, "bound", 2], + [IDBKeyRange, "lowerBound", 1], + [IDBKeyRange, "upperBound", 1], ]; for (const prototype of [IDBDatabase.prototype, IDBTransaction.prototype, IDBRequest.prototype]) { for (const name of ["addEventListener", "removeEventListener", "dispatchEvent"]) {