From 4b8ea1df63b505bbd6437a8d26a2e0618a85111b Mon Sep 17 00:00:00 2001 From: Ara Adkins Date: Tue, 2 Apr 2024 17:51:40 -0600 Subject: [PATCH] Patch serialize-javascript for mobile (#5) The library depended on randombytes which itself depended on safe-buffer. Unfortunately the buffer API is not supported on all variants of mobile platforms, so I ended up patching the serialization library to remove the dependency on safe-buffer. --- esbuild.config.mjs | 2 +- manifest.json | 2 +- package-lock.json | 39 ----- package.json | 9 +- decs.d.ts => src/decs.d.ts | 0 main.ts => src/main.ts | 2 +- src/serialize.ts | 342 +++++++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- versions.json | 3 +- 9 files changed, 352 insertions(+), 49 deletions(-) rename decs.d.ts => src/decs.d.ts (100%) rename main.ts => src/main.ts (99%) create mode 100644 src/serialize.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index bc0a6c4..18ee13f 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -14,7 +14,7 @@ const context = await esbuild.context({ banner: { js: banner, }, - entryPoints: ["main.ts"], + entryPoints: ["src/main.ts"], bundle: true, external: [ "obsidian", diff --git a/manifest.json b/manifest.json index 692e24e..8ba2c30 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "pkvs", "name": "Persistent Key-Value Store", - "version": "1.1.0", + "version": "1.1.1", "minAppVersion": "1.5.12", "description": "Provides a persistent key-value store for use in scripts, along with a portable web inspector.", "author": "Ara Adkins", diff --git a/package-lock.json b/package-lock.json index 845eef0..9386562 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,6 @@ "lint-staged": "^10.5.3", "obsidian": "latest", "prettier": "^3.1.1", - "serialize-javascript": "^6.0.1", "tslib": "2.6.2", "typescript": "5.3.3" } @@ -2702,15 +2701,6 @@ } ] }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2797,26 +2787,6 @@ "tslib": "^2.1.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -2838,15 +2808,6 @@ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true }, - "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 770d94d..56f92eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-pkvs", - "version": "1.1.0", + "version": "1.1.1", "description": "Provides a persistent key-value store for use in scripts in Obsidian.", "main": "main.js", "scripts": { @@ -19,13 +19,12 @@ "@typescript-eslint/parser": "6.17.0", "builtin-modules": "3.3.0", "esbuild": "0.19.11", + "husky": "^7.0.2", + "lint-staged": "^10.5.3", "obsidian": "latest", "prettier": "^3.1.1", - "serialize-javascript": "^6.0.1", "tslib": "2.6.2", - "typescript": "5.3.3", - "husky": "^7.0.2", - "lint-staged": "^10.5.3" + "typescript": "5.3.3" }, "dependencies": { "eruda": "^3.0.1", diff --git a/decs.d.ts b/src/decs.d.ts similarity index 100% rename from decs.d.ts rename to src/decs.d.ts diff --git a/main.ts b/src/main.ts similarity index 99% rename from main.ts rename to src/main.ts index 51fbe34..2b8dcf9 100644 --- a/main.ts +++ b/src/main.ts @@ -10,7 +10,7 @@ import { ToggleComponent, } from "obsidian"; -import serializeJS from "serialize-javascript"; +import { serializeJS } from "./serialize"; import erudaBenchmark from "eruda-benchmark"; import erudaCode from "eruda-code"; diff --git a/src/serialize.ts b/src/serialize.ts new file mode 100644 index 0000000..9f90c54 --- /dev/null +++ b/src/serialize.ts @@ -0,0 +1,342 @@ +/* +Copyright (c) 2014, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Modified by @iamrecursion (Ara Adkins) to not depend on random-bytes. +*/ + +"use strict"; + +// Generate an internal UID to make the regexp pattern harder to guess. +var UID_LENGTH = 16; +var UID = generateUID(); +var PLACE_HOLDER_REGEXP = new RegExp( + '(\\\\)?"@__(F|R|D|M|S|A|U|I|B|L)-' + UID + '-(\\d+)__@"', + "g", +); + +var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g; +var IS_PURE_FUNCTION = /function.*?\(/; +var IS_ARROW_FUNCTION = /.*?=>.*?/; +var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g; + +var RESERVED_SYMBOLS = ["*", "async"]; + +// Mapping of unsafe HTML and invalid JavaScript line terminator chars to their +// Unicode char counterparts which are safe to use in JavaScript strings. +var ESCAPED_CHARS: StringIndexable = { + "<": "\\u003C", + ">": "\\u003E", + "/": "\\u002F", + "\u2028": "\\u2028", + "\u2029": "\\u2029", +}; + +function escapeUnsafeChars(unsafeChar: string): string { + return ESCAPED_CHARS[unsafeChar]; +} + +function generateUID() { + var bytes: Uint8Array = new Uint8Array(UID_LENGTH); + crypto.getRandomValues(bytes); + var result = ""; + for (var i = 0; i < UID_LENGTH; ++i) { + const index = bytes[i]; + if (index !== undefined) { + result += index.toString(16); + } + } + return result; +} + +interface StringIndexable { + [index: string]: any; +} + +interface SerializeJSOptions { + /** + * This option is the same as the space argument that can be passed to JSON.stringify. + * It can be used to add whitespace and indentation to the serialized output to make it more readable. + */ + space?: string | number | undefined; + /** + * This option is a signal to serialize() that the object being serialized does not contain any function or regexps values. + * This enables a hot-path that allows serialization to be over 3x faster. + * If you're serializing a lot of data, and know its pure JSON, then you can enable this option for a speed-up. + */ + isJSON?: boolean | undefined; + /** + * This option is to signal serialize() that we want to do a straight conversion, without the XSS protection. + * This options needs to be explicitly set to true. HTML characters and JavaScript line terminators will not be escaped. + * You will have to roll your own. + */ + unsafe?: true | undefined; + /** + * This option is to signal serialize() that we do not want serialize JavaScript function. + * Just treat function like JSON.stringify do, but other features will work as expected. + */ + ignoreFunction?: boolean | undefined; +} + +function deleteFunctions(obj: StringIndexable) { + var functionKeys: string[] = []; + for (var key in obj) { + if (typeof obj[key] === "function") { + functionKeys.push(key); + } + } + for (var i = 0; i < functionKeys.length; i++) { + const key: string | undefined = functionKeys[i]; + + if (key) { + delete obj[key]; + } + } +} + +function serializeJS(obj: any, options?: SerializeJSOptions): string { + options || (options = {}); + + // Backwards-compatibility for `space` as the second argument. + if (typeof options === "number" || typeof options === "string") { + options = { space: options }; + } + + var functions: any[] = []; + var regexps: any[] = []; + var dates: any[] = []; + var maps: any[] = []; + var sets: any[] = []; + var arrays: any[] = []; + var undefs: any[] = []; + var infinities: any[] = []; + var bigInts: any[] = []; + var urls: any[] = []; + + // Returns placeholders for functions and regexps (identified by index) + // which are later replaced by their string representation. + function replacer(this: StringIndexable, key: string, value: any) { + // For nested function + if (options !== undefined && options.ignoreFunction) { + deleteFunctions(value); + } + + if (!value && value !== undefined && value !== BigInt(0)) { + return value; + } + + // If the value is an object w/ a toJSON method, toJSON is called before + // the replacer runs, so we use this[key] to get the non-toJSONed value. + var origValue = this[key]; + var type = typeof origValue; + + if (type === "object") { + if (origValue instanceof RegExp) { + return "@__R-" + UID + "-" + (regexps.push(origValue) - 1) + "__@"; + } + + if (origValue instanceof Date) { + return "@__D-" + UID + "-" + (dates.push(origValue) - 1) + "__@"; + } + + if (origValue instanceof Map) { + return "@__M-" + UID + "-" + (maps.push(origValue) - 1) + "__@"; + } + + if (origValue instanceof Set) { + return "@__S-" + UID + "-" + (sets.push(origValue) - 1) + "__@"; + } + + if (origValue instanceof Array) { + var isSparse = + origValue.filter(function () { + return true; + }).length !== origValue.length; + if (isSparse) { + return "@__A-" + UID + "-" + (arrays.push(origValue) - 1) + "__@"; + } + } + + if (origValue instanceof URL) { + return "@__L-" + UID + "-" + (urls.push(origValue) - 1) + "__@"; + } + } + + if (type === "function") { + return "@__F-" + UID + "-" + (functions.push(origValue) - 1) + "__@"; + } + + if (type === "undefined") { + return "@__U-" + UID + "-" + (undefs.push(origValue) - 1) + "__@"; + } + + if (type === "number" && !isNaN(origValue) && !isFinite(origValue)) { + return "@__I-" + UID + "-" + (infinities.push(origValue) - 1) + "__@"; + } + + if (type === "bigint") { + return "@__B-" + UID + "-" + (bigInts.push(origValue) - 1) + "__@"; + } + + return value; + } + + function serializeFunc(fn: Function) { + var serializedFn = fn.toString(); + if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) { + throw new TypeError("Serializing native function: " + fn.name); + } + + // pure functions, example: {key: function() {}} + if (IS_PURE_FUNCTION.test(serializedFn)) { + return serializedFn; + } + + // arrow functions, example: arg1 => arg1+5 + if (IS_ARROW_FUNCTION.test(serializedFn)) { + return serializedFn; + } + + var argsStartsAt = serializedFn.indexOf("("); + var def = serializedFn + .substr(0, argsStartsAt) + .trim() + .split(" ") + .filter(function (val) { + return val.length > 0; + }); + + var nonReservedSymbols = def.filter(function (val) { + return RESERVED_SYMBOLS.indexOf(val) === -1; + }); + + // enhanced literal objects, example: {key() {}} + if (nonReservedSymbols.length > 0) { + return ( + (def.indexOf("async") > -1 ? "async " : "") + + "function" + + (def.join("").indexOf("*") > -1 ? "*" : "") + + serializedFn.substr(argsStartsAt) + ); + } + + // arrow functions + return serializedFn; + } + + // Check if the parameter is function + if (options.ignoreFunction && typeof obj === "function") { + obj = undefined; + } + // Protects against `JSON.stringify()` returning `undefined`, by serializing + // to the literal string: "undefined". + if (obj === undefined) { + return String(obj); + } + + var str; + + // Creates a JSON string representation of the value. + // NOTE: Node 0.12 goes into slow mode with extra JSON.stringify() args. + if (options.isJSON && !options.space) { + str = JSON.stringify(obj); + } else { + str = JSON.stringify(obj, options.isJSON ? undefined : replacer, options.space); + } + + // Protects against `JSON.stringify()` returning `undefined`, by serializing + // to the literal string: "undefined". + if (typeof str !== "string") { + return String(str); + } + + // Replace unsafe HTML and invalid JavaScript line terminator chars with + // their safe Unicode char counterpart. This _must_ happen before the + // regexps and functions are serialized and added back to the string. + if (options.unsafe !== true) { + str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars); + } + + if ( + functions.length === 0 && + regexps.length === 0 && + dates.length === 0 && + maps.length === 0 && + sets.length === 0 && + arrays.length === 0 && + undefs.length === 0 && + infinities.length === 0 && + bigInts.length === 0 && + urls.length === 0 + ) { + return str; + } + + // Replaces all occurrences of function, regexp, date, map and set placeholders in the + // JSON string with their string representations. If the original value can + // not be found, then `undefined` is used. + return str.replace(PLACE_HOLDER_REGEXP, function (match, backSlash, type, valueIndex) { + // The placeholder may not be preceded by a backslash. This is to prevent + // replacing things like `"a\"@__R--0__@"` and thus outputting + // invalid JS. + if (backSlash) { + return match; + } + + if (type === "D") { + return 'new Date("' + dates[valueIndex].toISOString() + '")'; + } + + if (type === "R") { + return ( + "new RegExp(" + + serializeJS(regexps[valueIndex].source) + + ', "' + + regexps[valueIndex].flags + + '")' + ); + } + + if (type === "M") { + return "new Map(" + serializeJS(Array.from(maps[valueIndex].entries()), options) + ")"; + } + + if (type === "S") { + return "new Set(" + serializeJS(Array.from(sets[valueIndex].values()), options) + ")"; + } + + if (type === "A") { + return ( + "Array.prototype.slice.call(" + + serializeJS( + Object.assign({ length: arrays[valueIndex].length }, arrays[valueIndex]), + options, + ) + + ")" + ); + } + + if (type === "U") { + return "undefined"; + } + + if (type === "I") { + return infinities[valueIndex]; + } + + if (type === "B") { + return 'BigInt("' + bigInts[valueIndex] + '")'; + } + + if (type === "L") { + return "new URL(" + serializeJS(urls[valueIndex].toString(), options) + ")"; + } + + var fn = functions[valueIndex]; + + return serializeFunc(fn); + }); +} + +export { serializeJS, SerializeJSOptions }; diff --git a/tsconfig.json b/tsconfig.json index 78948c2..b93cb15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,5 @@ "strict": true, "useUnknownInCatchVariables": true }, - "include": ["**/*.ts"] + "include": ["src/**.ts"] } diff --git a/versions.json b/versions.json index 717d45f..0cda849 100644 --- a/versions.json +++ b/versions.json @@ -3,5 +3,6 @@ "1.0.1": "1.5.1", "1.0.2": "1.5.1", "1.0.3": "1.5.1", - "1.1.0": "1.5.12" + "1.1.0": "1.5.12", + "1.1.1": "1.5.12" }