mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 08:01:05 +00:00
fix(indexeddb): protect key range bounds and validate receivers
Store key range bounds and open flags in private V8 snapshots. Expose readonly prototype accessors and use native WebIDL receiver branding for getters and includes, including cross-realm calls and author Proxies. Queries and deletion now ignore public attribute shadows and mutations of returned keys. Remove the redundant range marker and runtime registry. Correct the open flag of unbounded endpoints and static method lengths. Cover bounded, unbounded and empty queries in upgrade, running and queued transactions, plus getter exception ordering and callee-realm errors. Validation: cargo fmt, workspace Clippy with -D warnings, and all 19,185 nextest tests passed (13 skipped). In 242 official IndexedDB WPT cases, passes increase from 230 to 232 with no subtest regressions.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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<IdbKeyRangeQuery> {
|
||||
let object = v8::Local::<v8::Object>::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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+16
-4
@@ -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: (),
|
||||
|
||||
@@ -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";
|
||||
|
||||
+1
-1
@@ -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());
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<IndexedDbObjectId, IndexedDbCursorLifecycleState>,
|
||||
object_stores: BTreeMap<IndexedDbObjectId, IndexedDbObjectStoreLifecycleState>,
|
||||
indexes: BTreeMap<IndexedDbObjectId, IndexedDbIndexLifecycleState>,
|
||||
key_ranges: BTreeMap<IndexedDbObjectId, IndexedDbKeyRangeLifecycleState>,
|
||||
}
|
||||
|
||||
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<v8::Local<'s, v8::Value>> {
|
||||
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>,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"]) {
|
||||
|
||||
Reference in New Issue
Block a user