From 564e4f8710388ab2ae40c959c497f2e0260199ed Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 24 Feb 2021 17:33:07 +0100 Subject: idb: encapsulate non-JSON data correctly --- packages/idb-bridge/src/MemoryBackend.ts | 25 +++++--- packages/idb-bridge/src/bridge-idb.ts | 20 +++--- .../src/idb-wpt-ported/idbindex-openCursor.test.ts | 30 ++++----- packages/idb-bridge/src/util/canInjectKey.ts | 50 --------------- packages/idb-bridge/src/util/deepEquals.ts | 72 ---------------------- packages/idb-bridge/src/util/injectKey.ts | 69 --------------------- packages/idb-bridge/src/util/makeStoreKeyValue.ts | 50 ++++++++++++++- .../idb-bridge/src/util/structuredClone.test.ts | 39 ++++++++++++ packages/idb-bridge/src/util/structuredClone.ts | 69 +++++++++++---------- 9 files changed, 166 insertions(+), 258 deletions(-) delete mode 100644 packages/idb-bridge/src/util/canInjectKey.ts delete mode 100644 packages/idb-bridge/src/util/deepEquals.ts delete mode 100644 packages/idb-bridge/src/util/injectKey.ts create mode 100644 packages/idb-bridge/src/util/structuredClone.test.ts (limited to 'packages/idb-bridge') diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts index 68f60f756..9233e8d88 100644 --- a/packages/idb-bridge/src/MemoryBackend.ts +++ b/packages/idb-bridge/src/MemoryBackend.ts @@ -27,13 +27,12 @@ import { StoreLevel, RecordStoreResponse, } from "./backend-interface"; -import { structuredClone, structuredRevive } from "./util/structuredClone"; import { - InvalidStateError, - InvalidAccessError, - ConstraintError, - DataError, -} from "./util/errors"; + structuredClone, + structuredEncapsulate, + structuredRevive, +} from "./util/structuredClone"; +import { ConstraintError, DataError } from "./util/errors"; import BTree, { ISortedMapF } from "./tree/b+tree"; import { compareKeys } from "./util/cmp"; import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue"; @@ -269,6 +268,12 @@ export class MemoryBackend implements Backend { ); } + if (typeof data !== "object") { + throw Error("db dump corrupt"); + } + + data = structuredRevive(data); + this.databases = {}; for (const dbName of Object.keys(data.databases)) { @@ -390,7 +395,7 @@ export class MemoryBackend implements Backend { }; dbDumps[dbName] = dbDump; } - return { databases: dbDumps }; + return structuredEncapsulate({ databases: dbDumps }); } async getDatabases(): Promise<{ name: string; version: number }[]> { @@ -1322,7 +1327,7 @@ export class MemoryBackend implements Backend { console.error("request was", req); throw Error("invariant violated during read"); } - values.push(structuredClone(result.value)); + values.push(result.value); } } } else { @@ -1468,10 +1473,9 @@ export class MemoryBackend implements Backend { } let storeKeyResult: StoreKeyResult; - const revivedValue = structuredRevive(storeReq.value); try { storeKeyResult = makeStoreKeyValue( - revivedValue, + storeReq.value, storeReq.key, keygen, autoIncrement, @@ -1506,6 +1510,7 @@ export class MemoryBackend implements Backend { } const objectStoreRecord: ObjectStoreRecord = { + // FIXME: We should serialize the key here, not just clone it. primaryKey: structuredClone(key), value: structuredClone(value), }; diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts index 0c309b421..1fa21c7d5 100644 --- a/packages/idb-bridge/src/bridge-idb.ts +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -64,8 +64,6 @@ import { openPromise } from "./util/openPromise"; import queueTask from "./util/queueTask"; import { structuredClone, - structuredEncapsulate, - structuredRevive, } from "./util/structuredClone"; import { validateKeyPath } from "./util/validateKeyPath"; import { valueToKey } from "./util/valueToKey"; @@ -249,7 +247,7 @@ export class BridgeIDBCursor implements IDBCursor { this._primaryKey = response.primaryKeys![0]; if (!this._keyOnly) { - this._value = structuredRevive(response.values![0]); + this._value = response.values![0]; } this._gotValue = true; @@ -294,7 +292,7 @@ export class BridgeIDBCursor implements IDBCursor { const storeReq: RecordStoreRequest = { key: this._primaryKey, - value: structuredEncapsulate(value), + value, objectStoreName: this._objectStoreName, storeLevel: StoreLevel.UpdateExisting, }; @@ -1216,7 +1214,7 @@ export class BridgeIDBIndex implements IDBIndex { if (!values) { throw Error("invariant violated"); } - return structuredRevive(values[0]); + return values[0]; }; return this._objectStore._transaction._execRequestAsync({ @@ -1260,7 +1258,7 @@ export class BridgeIDBIndex implements IDBIndex { if (!values) { throw Error("invariant violated"); } - return values.map((x) => structuredRevive(x)); + return values; }; return this._objectStore._transaction._execRequestAsync({ @@ -1641,7 +1639,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { const result = await this._backend.storeRecord(btx, { objectStoreName: this._name, key: key, - value: structuredEncapsulate(value), + value, storeLevel: overwrite ? StoreLevel.AllowOverwrite : StoreLevel.NoOverwrite, @@ -1771,7 +1769,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (values.length !== 1) { throw Error("invariant violated"); } - return structuredRevive(values[0]); + return values[0]; }; return this._transaction._execRequestAsync({ @@ -1830,7 +1828,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (!values) { throw Error("invariant violated"); } - return values.map((x) => structuredRevive(x)); + return values; }; return this._transaction._execRequestAsync({ @@ -1891,7 +1889,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (primaryKeys.length !== 1) { throw Error("invariant violated"); } - return structuredRevive(primaryKeys[0]); + return primaryKeys[0]; }; return this._transaction._execRequestAsync({ @@ -1956,7 +1954,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (!primaryKeys) { throw Error("invariant violated"); } - return primaryKeys.map((x) => structuredRevive(x)); + return primaryKeys; }; return this._transaction._execRequestAsync({ diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbindex-openCursor.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbindex-openCursor.test.ts index 2dcab6034..f4515b69e 100644 --- a/packages/idb-bridge/src/idb-wpt-ported/idbindex-openCursor.test.ts +++ b/packages/idb-bridge/src/idb-wpt-ported/idbindex-openCursor.test.ts @@ -56,25 +56,27 @@ test.cb("WPT test idbindex-openCursor2.htm", (t) => { }; }); - // IDBIndex.openCursor() - throw InvalidStateError on index deleted by aborted upgrade test.cb("WPT test idbindex-openCursor3.htm", (t) => { var db; -var open_rq = createdb(t); -open_rq.onupgradeneeded = function(e: any) { - db = e.target.result; - var store = db.createObjectStore("store", { keyPath: "key" }); - var index = store.createIndex("index", "indexedProperty"); - store.add({ key: 1, indexedProperty: "data" }); + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + var store = db.createObjectStore("store", { keyPath: "key" }); + var index = store.createIndex("index", "indexedProperty"); + store.add({ key: 1, indexedProperty: "data" }); - e.target.transaction.abort(); + e.target.transaction.abort(); - t.throws(() => { - console.log("index before openCursor", index); - index.openCursor(); - }, { name: "InvalidStateError"}); + t.throws( + () => { + console.log("index before openCursor", index); + index.openCursor(); + }, + { name: "InvalidStateError" }, + ); - t.end(); -} + t.end(); + }; }); diff --git a/packages/idb-bridge/src/util/canInjectKey.ts b/packages/idb-bridge/src/util/canInjectKey.ts deleted file mode 100644 index 09ecbd3ad..000000000 --- a/packages/idb-bridge/src/util/canInjectKey.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright 2017 Jeremy Scheff - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - or implied. See the License for the specific language governing - permissions and limitations under the License. -*/ - -import { IDBKeyPath } from "../idbtypes"; - -// http://w3c.github.io/IndexedDB/#check-that-a-key-could-be-injected-into-a-value -const canInjectKey = (keyPath: IDBKeyPath, value: any) => { - if (Array.isArray(keyPath)) { - // tslint:disable-next-line max-line-length - throw new Error( - "The key paths used in this section are always strings and never sequences, since it is not possible to create a object store which has a key generator and also has a key path that is a sequence.", - ); - } - - const identifiers = keyPath.split("."); - if (identifiers.length === 0) { - throw new Error("Assert: identifiers is not empty"); - } - identifiers.pop(); - - for (const identifier of identifiers) { - if (typeof value !== "object" && !Array.isArray(value)) { - return false; - } - - const hop = value.hasOwnProperty(identifier); - if (!hop) { - return true; - } - - value = value[identifier]; - } - - return typeof value === "object" || Array.isArray(value); -}; - -export default canInjectKey; diff --git a/packages/idb-bridge/src/util/deepEquals.ts b/packages/idb-bridge/src/util/deepEquals.ts deleted file mode 100644 index bb7c0269c..000000000 --- a/packages/idb-bridge/src/util/deepEquals.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright (c) 2017 Evgeny Poberezkin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -const isArray = Array.isArray; -const keyList = Object.keys; -const hasProp = Object.prototype.hasOwnProperty; - -export function deepEquals(a: any, b: any): boolean { - if (a === b) return true; - - if (a && b && typeof a == "object" && typeof b == "object") { - const arrA = isArray(a); - const arrB = isArray(b); - let i; - let length; - let key; - - if (arrA && arrB) { - length = a.length; - if (length != b.length) return false; - for (i = length; i-- !== 0; ) if (!deepEquals(a[i], b[i])) return false; - return true; - } - - if (arrA != arrB) return false; - - const dateA = a instanceof Date; - const dateB = b instanceof Date; - if (dateA != dateB) return false; - if (dateA && dateB) return a.getTime() == b.getTime(); - - const regexpA = a instanceof RegExp; - const regexpB = b instanceof RegExp; - if (regexpA != regexpB) return false; - if (regexpA && regexpB) return a.toString() == b.toString(); - - const keys = keyList(a); - length = keys.length; - - if (length !== keyList(b).length) return false; - - for (i = length; i-- !== 0; ) if (!hasProp.call(b, keys[i])) return false; - - for (i = length; i-- !== 0; ) { - key = keys[i]; - if (!deepEquals(a[key], b[key])) return false; - } - - return true; - } - - return a !== a && b !== b; -} diff --git a/packages/idb-bridge/src/util/injectKey.ts b/packages/idb-bridge/src/util/injectKey.ts deleted file mode 100644 index 02acfaa4c..000000000 --- a/packages/idb-bridge/src/util/injectKey.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - Copyright 2017 Jeremy Scheff - Copyright 2019 Florian Dold - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - or implied. See the License for the specific language governing - permissions and limitations under the License. -*/ - -import { IDBKeyPath, IDBValidKey } from "../idbtypes"; -import { structuredClone } from "./structuredClone"; - -export function injectKey( - keyPath: IDBKeyPath | IDBKeyPath[], - value: any, - key: IDBValidKey, -): any { - if (Array.isArray(keyPath)) { - // tslint:disable-next-line max-line-length - throw new Error( - "The key paths used in this section are always strings and never sequences, since it is not possible to create a object store which has a key generator and also has a key path that is a sequence.", - ); - } - - const newValue = structuredClone(value); - - // Position inside the new value where we'll place the key eventually. - let ptr = newValue; - - const identifiers = keyPath.split("."); - if (identifiers.length === 0) { - throw new Error("Assert: identifiers is not empty"); - } - - const lastIdentifier = identifiers.pop(); - - if (lastIdentifier === null || lastIdentifier === undefined) { - throw Error(); - } - - for (const identifier of identifiers) { - if (typeof ptr !== "object" && !Array.isArray(ptr)) { - throw new Error("can't inject key"); - } - - const hop = value.hasOwnProperty(identifier); - if (!hop) { - ptr[identifier] = {}; - } - - ptr = ptr[identifier]; - } - - if (!(typeof ptr === "object" || Array.isArray(ptr))) { - throw new Error("can't inject key"); - } - - ptr[lastIdentifier] = structuredClone(key); - - return newValue; -} diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.ts index 442a69ff5..243e46e04 100644 --- a/packages/idb-bridge/src/util/makeStoreKeyValue.ts +++ b/packages/idb-bridge/src/util/makeStoreKeyValue.ts @@ -18,7 +18,6 @@ import { extractKey } from "./extractKey"; import { DataError } from "./errors"; import { valueToKey } from "./valueToKey"; import { structuredClone } from "./structuredClone"; -import { injectKey } from "./injectKey"; import { IDBKeyPath, IDBValidKey } from "../idbtypes"; export interface StoreKeyResult { @@ -27,6 +26,55 @@ export interface StoreKeyResult { value: any; } +export function injectKey( + keyPath: IDBKeyPath | IDBKeyPath[], + value: any, + key: IDBValidKey, +): any { + if (Array.isArray(keyPath)) { + throw new Error( + "The key paths used in this section are always strings and never sequences, since it is not possible to create a object store which has a key generator and also has a key path that is a sequence.", + ); + } + + const newValue = structuredClone(value); + + // Position inside the new value where we'll place the key eventually. + let ptr = newValue; + + const identifiers = keyPath.split("."); + if (identifiers.length === 0) { + throw new Error("Assert: identifiers is not empty"); + } + + const lastIdentifier = identifiers.pop(); + + if (lastIdentifier === null || lastIdentifier === undefined) { + throw Error(); + } + + for (const identifier of identifiers) { + if (typeof ptr !== "object" && !Array.isArray(ptr)) { + throw new Error("can't inject key"); + } + + const hop = value.hasOwnProperty(identifier); + if (!hop) { + ptr[identifier] = {}; + } + + ptr = ptr[identifier]; + } + + if (!(typeof ptr === "object" || Array.isArray(ptr))) { + throw new Error("can't inject key"); + } + + ptr[lastIdentifier] = structuredClone(key); + + return newValue; +} + export function makeStoreKeyValue( value: any, key: IDBValidKey | undefined, diff --git a/packages/idb-bridge/src/util/structuredClone.test.ts b/packages/idb-bridge/src/util/structuredClone.test.ts new file mode 100644 index 000000000..58a7f32c1 --- /dev/null +++ b/packages/idb-bridge/src/util/structuredClone.test.ts @@ -0,0 +1,39 @@ +/* + Copyright 2019 Florian Dold + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + or implied. See the License for the specific language governing + permissions and limitations under the License. +*/ + +import test, { ExecutionContext } from "ava"; +import { structuredClone } from "./structuredClone"; + +function checkClone(t: ExecutionContext, x: any): void { + t.deepEqual(structuredClone(x), x); +} + +test("structured clone", (t) => { + checkClone(t, "foo"); + checkClone(t, [1, 2]); + checkClone(t, { x1: "foo" }); + checkClone(t, new Date()); + checkClone(t, [new Date()]); + checkClone(t, undefined); + checkClone(t, [undefined]); +}); + +test("structured clone (cycles)", (t) => { + const obj1: any[] = [1, 2]; + obj1.push(obj1); + const obj1Clone = structuredClone(obj1); + t.is(obj1Clone, obj1Clone[2]); +}); diff --git a/packages/idb-bridge/src/util/structuredClone.ts b/packages/idb-bridge/src/util/structuredClone.ts index 215681a2d..4ba97dd7a 100644 --- a/packages/idb-bridge/src/util/structuredClone.ts +++ b/packages/idb-bridge/src/util/structuredClone.ts @@ -14,8 +14,6 @@ permissions and limitations under the License. */ -import { DataCloneError } from "./errors"; - const { toString: toStr } = {}; const hasOwn = {}.hasOwnProperty; const getProto = Object.getPrototypeOf; @@ -46,11 +44,6 @@ function hasConstructorOf(a: any, b: any) { return false; } -/** - * - * @param {any} val - * @returns {boolean} - */ function isPlainObject(val: any): boolean { if (!val || toStringTag(val) !== "Object") { return false; @@ -157,11 +150,7 @@ export function structuredEncapsulate(val: any): any { const outRoot = {}; const types: Array<[string[], string]> = []; let res; - try { - res = internalEncapsulate(val, outRoot, [], new Map(), types); - } catch (e) { - throw new DataCloneError(); - } + res = internalEncapsulate(val, outRoot, [], new Map(), types); if (res === null) { return res; } @@ -218,32 +207,50 @@ export function internalStructuredRevive(val: any): any { const last = path[path.length - 1]; obj[last] = f(obj[last]); } + function lookupPath(path: string[]): any { + let obj = outRoot; + for (const n of path) { + obj = obj[n]; + } + return obj; + } for (const [path, type] of types) { - if (type === "bigint") { - mutatePath(path, (x) => BigInt(x)); - } else if (type === "array") { - mutatePath(path, (x) => { - const newArr: any = []; - for (const k in x) { - newArr[k] = x[k]; - } - return newArr; - }); - } else if (type === "date") { - mutatePath(path, (x) => new Date(x)); - } else { - throw Error("type not implemented"); + switch (type) { + case "bigint": { + mutatePath(path, (x) => BigInt(x)); + break; + } + case "array": { + mutatePath(path, (x) => { + const newArr: any = []; + for (const k in x) { + newArr[k] = x[k]; + } + return newArr; + }); + break; + } + case "date": { + mutatePath(path, (x) => new Date(x)); + break; + } + case "undef": { + mutatePath(path, (x) => undefined); + break; + } + case "ref": { + mutatePath(path, (x) => lookupPath(x)); + break; + } + default: + throw Error(`type '${type}' not implemented`); } } return outRoot; } export function structuredRevive(val: any): any { - try { - return internalStructuredRevive(val); - } catch (e) { - throw new DataCloneError(); - } + return internalStructuredRevive(val); } /** -- cgit v1.2.3