idb: encapsulate non-JSON data correctly

This commit is contained in:
Florian Dold 2021-02-24 17:33:07 +01:00
parent bc7956c2ba
commit 564e4f8710
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
10 changed files with 167 additions and 317 deletions

View File

@ -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),
};

View File

@ -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({

View File

@ -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();
};
});

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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]);
});

View File

@ -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);
}
/**

View File

@ -4,8 +4,6 @@ importers:
packages/idb-bridge:
dependencies:
tslib: 2.1.0
typeson: 5.18.2
typeson-registry: 1.0.0-alpha.38
devDependencies:
'@rollup/plugin-commonjs': 17.1.0_rollup@2.37.1
'@rollup/plugin-json': 4.1.0_rollup@2.37.1
@ -29,8 +27,6 @@ importers:
rollup: ^2.37.1
tslib: ^2.1.0
typescript: ^4.1.3
typeson: ^5.18.2
typeson-registry: ^1.0.0-alpha.38
packages/pogen:
dependencies:
'@types/node': 14.14.22
@ -1240,14 +1236,6 @@ packages:
/balanced-match/1.0.0:
resolution:
integrity: sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
/base64-arraybuffer-es6/0.6.0:
dev: false
engines:
node: '>=0.10.0'
peerDependencies:
core-js-bundle: ^3.6.5
resolution:
integrity: sha512-57nLqKj4ShsDwFJWJsM4sZx6u60WbCge35rWRSevUwqxDtRwwxiKAO800zD2upPv4CfdWjQp//wSLar35nDKvA==
/base64-js/1.5.1:
dev: true
resolution:
@ -3338,10 +3326,6 @@ packages:
dev: true
resolution:
integrity: sha1-QVxEePK8wwEgwizhDtMib30+GOA=
/lodash.sortby/4.7.0:
dev: false
resolution:
integrity: sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
/lodash/4.17.20:
dev: true
resolution:
@ -4064,6 +4048,7 @@ packages:
resolution:
integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
/punycode/2.1.1:
dev: true
engines:
node: '>=6'
resolution:
@ -4829,14 +4814,6 @@ packages:
node: '>=8.0'
resolution:
integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
/tr46/2.0.2:
dependencies:
punycode: 2.1.1
dev: false
engines:
node: '>=8'
resolution:
integrity: sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==
/trim-off-newlines/1.0.1:
dev: true
engines:
@ -4944,25 +4921,6 @@ packages:
hasBin: true
resolution:
integrity: sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
/typeson-registry/1.0.0-alpha.38:
dependencies:
base64-arraybuffer-es6: 0.6.0
typeson: 5.18.2
whatwg-url: 8.4.0
dev: false
engines:
node: '>=10.0.0'
resolution:
integrity: sha512-6lt2IhbNT9hyow5hljZqjWtVDXBIaC1X8bBGlBva0Pod2f42g23bVqww09ruquwSC48I8BSSCPi+B2dFHM5ihQ==
/typeson/5.18.2:
dev: false
engines:
node: '>=0.1.14'
peerDependencies:
core-js-bundle: ^3.6.4
regenerator-runtime: ^0.13.3
resolution:
integrity: sha512-Vetd+OGX05P4qHyHiSLdHZ5Z5GuQDrHHwSdjkqho9NSCYVSLSfRMjklD/unpHH8tXBR9Z/R05rwJSuMpMFrdsw==
/uglify-js/3.12.5:
dev: true
engines:
@ -5066,28 +5024,12 @@ packages:
dev: true
resolution:
integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
/webidl-conversions/6.1.0:
dev: false
engines:
node: '>=10.4'
resolution:
integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
/well-known-symbols/2.0.0:
dev: true
engines:
node: '>=6'
resolution:
integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==
/whatwg-url/8.4.0:
dependencies:
lodash.sortby: 4.7.0
tr46: 2.0.2
webidl-conversions: 6.1.0
dev: false
engines:
node: '>=10'
resolution:
integrity: sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==
/which-module/2.0.0:
dev: true
resolution: