From bbf86c2df544f3855603012b70d2be2200c7dbf3 Mon Sep 17 00:00:00 2001 From: Loris Sigrist Date: Sun, 17 Sep 2023 21:01:47 +0200 Subject: [PATCH 1/3] Add support for ArrayBuffers and TypedArrays --- playrgound.js | 23 ++++++++++ src/base64.js | 110 +++++++++++++++++++++++++++++++++++++++++++++++ src/parse.js | 27 ++++++++++++ src/stringify.js | 28 ++++++++++++ src/types.d.ts | 1 + src/uneval.js | 37 ++++++++++++++++ test/test.js | 12 ++++++ 7 files changed, 238 insertions(+) create mode 100644 playrgound.js create mode 100644 src/base64.js create mode 100644 src/types.d.ts diff --git a/playrgound.js b/playrgound.js new file mode 100644 index 0000000..a943d19 --- /dev/null +++ b/playrgound.js @@ -0,0 +1,23 @@ +import { stringify } from "./src/stringify.js" +import { parse} from "./src/parse.js" + + +const thing = new Uint8Array(3); +thing[0] = 1; +thing[1] = 2; +thing[2] = 3; + +const otherThing = new Float32Array(10); +otherThing[0] = -Infinity; + + +const a = { + foo: thing, + bar: otherThing, + buff: otherThing.buffer +} + + +const stringified = stringify(a); +const parsed = parse(stringified); +console.log(a, stringified, parsed); diff --git a/src/base64.js b/src/base64.js new file mode 100644 index 0000000..23bd6b6 --- /dev/null +++ b/src/base64.js @@ -0,0 +1,110 @@ +/** + * Base64 Encodes an arraybuffer + * @param {ArrayBuffer} arraybuffer + * @returns {string} + */ +export function encode64(arraybuffer) { + const dv = new DataView(arraybuffer); + let binaryString = ""; + + for (let i = 0; i < arraybuffer.byteLength; i++) { + binaryString += String.fromCharCode(dv.getUint8(i)); + } + + return binaryToAscii(binaryString); +} + +/** + * Decodes a base64 string into an arraybuffer + * @param {string} string + * @returns {ArrayBuffer} + */ +export function decode64(string) { + const binaryString = asciiToBinary(string); + const arraybuffer = new ArrayBuffer(binaryString.length); + const dv = new DataView(arraybuffer); + + for (let i = 0; i < arraybuffer.byteLength; i++) { + dv.setUint8(i, binaryString.charCodeAt(i)); + } + + return arraybuffer; +} + +const KEY_STRING = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/** + * Substitute for atob since it's deprecated in node. + * Does not do any input validation. + * + * @see https://github.com/jsdom/abab/blob/master/lib/atob.js + * + * @param {string} data + * @returns {string} + */ +function asciiToBinary(data) { + if (data.length % 4 === 0) { + data = data.replace(/==?$/, ""); + } + + let output = ""; + let buffer = 0; + let accumulatedBits = 0; + + for (let i = 0; i < data.length; i++) { + buffer <<= 6; + buffer |= KEY_STRING.indexOf(data[i]); + accumulatedBits += 6; + if (accumulatedBits === 24) { + output += String.fromCharCode((buffer & 0xff0000) >> 16); + output += String.fromCharCode((buffer & 0xff00) >> 8); + output += String.fromCharCode(buffer & 0xff); + buffer = accumulatedBits = 0; + } + } + if (accumulatedBits === 12) { + buffer >>= 4; + output += String.fromCharCode(buffer); + } else if (accumulatedBits === 18) { + buffer >>= 2; + output += String.fromCharCode((buffer & 0xff00) >> 8); + output += String.fromCharCode(buffer & 0xff); + } + return output; +} + +/** + * Substitute for btoa since it's deprecated in node. + * Does not do any input validation. + * + * @see https://github.com/jsdom/abab/blob/master/lib/btoa.js + * + * @param {string} str + * @returns {string} + */ +function binaryToAscii(str) { + let out = ""; + for (let i = 0; i < str.length; i += 3) { + /** @type {[number, number, number, number]} */ + const groupsOfSix = [undefined, undefined, undefined, undefined]; + groupsOfSix[0] = str.charCodeAt(i) >> 2; + groupsOfSix[1] = (str.charCodeAt(i) & 0x03) << 4; + if (str.length > i + 1) { + groupsOfSix[1] |= str.charCodeAt(i + 1) >> 4; + groupsOfSix[2] = (str.charCodeAt(i + 1) & 0x0f) << 2; + } + if (str.length > i + 2) { + groupsOfSix[2] |= str.charCodeAt(i + 2) >> 6; + groupsOfSix[3] = str.charCodeAt(i + 2) & 0x3f; + } + for (let j = 0; j < groupsOfSix.length; j++) { + if (typeof groupsOfSix[j] === "undefined") { + out += "="; + } else { + out += KEY_STRING[groupsOfSix[j]]; + } + } + } + return out; +} diff --git a/src/parse.js b/src/parse.js index 54cdeba..f0fe2c1 100644 --- a/src/parse.js +++ b/src/parse.js @@ -1,3 +1,4 @@ +import { decode64 } from './base64.js'; import { HOLE, NAN, @@ -101,6 +102,32 @@ export function unflatten(parsed, revivers) { } break; + case "Int8Array": + case "Uint8Array": + case "Uint8ClampedArray": + case "Int16Array": + case "Uint16Array": + case "Int32Array": + case "Uint32Array": + case "Float32Array": + case "Float64Array": + case "BigInt64Array": + case "BigUint64Array": { + const TypedArrayConstructor = globalThis[type]; + const base64 = value[1]; + const arraybuffer = decode64(base64); + const typedArray = new TypedArrayConstructor(arraybuffer); + hydrated[index] = typedArray; + break; + } + + case "ArrayBuffer": { + const base64 = value[1]; + const arraybuffer = decode64(base64); + hydrated[index] = arraybuffer; + break; + } + default: throw new Error(`Unknown type ${type}`); } diff --git a/src/stringify.js b/src/stringify.js index 8ce427e..ad8d467 100644 --- a/src/stringify.js +++ b/src/stringify.js @@ -13,6 +13,7 @@ import { POSITIVE_INFINITY, UNDEFINED } from './constants.js'; +import { encode64 } from './base64.js'; /** * Turn a value into a JSON string that can be parsed with `devalue.parse` @@ -133,6 +134,33 @@ export function stringify(value, reducers) { str += ']'; break; + case "Int8Array": + case "Uint8Array": + case "Uint8ClampedArray": + case "Int16Array": + case "Uint16Array": + case "Int32Array": + case "Uint32Array": + case "Float32Array": + case "Float64Array": + case "BigInt64Array": + case "BigUint64Array": { + /** @type {import("./types.js").TypedArray} */ + const typedArray = thing; + const base64 = encode64(typedArray.buffer); + str = '["' + type + '","' + base64 + '"]'; + break; + } + + case "ArrayBuffer": { + /** @type {ArrayBuffer} */ + const arraybuffer = thing; + const base64 = encode64(arraybuffer); + + str = `["ArrayBuffer","${base64}"]`; + break; + } + default: if (!is_plain_object(thing)) { throw new DevalueError( diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..5a2019f --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1 @@ +export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array; \ No newline at end of file diff --git a/src/uneval.js b/src/uneval.js index 21a98a4..46db734 100644 --- a/src/uneval.js +++ b/src/uneval.js @@ -80,6 +80,22 @@ export function uneval(value, replacer) { keys.pop(); } break; + + case "Int8Array": + case "Uint8Array": + case "Uint8ClampedArray": + case "Int16Array": + case "Uint16Array": + case "Int32Array": + case "Uint32Array": + case "Float32Array": + case "Float64Array": + case "BigInt64Array": + case "BigUint64Array": + return; + + case "ArrayBuffer": + return; default: if (!is_plain_object(thing)) { @@ -159,6 +175,27 @@ export function uneval(value, replacer) { case 'Set': case 'Map': return `new ${type}([${Array.from(thing).map(stringify).join(',')}])`; + + case "Int8Array": + case "Uint8Array": + case "Uint8ClampedArray": + case "Int16Array": + case "Uint16Array": + case "Int32Array": + case "Uint32Array": + case "Float32Array": + case "Float64Array": + case "BigInt64Array": + case "BigUint64Array": { + /** @type {import("./types.js").TypedArray} */ + const typedArray = thing; + return `new ${type}([${typedArray.toString()}])`; + } + + case "ArrayBuffer": { + const ui8 = new Uint8Array(thing); + return `new Uint8Array([${ui8.toString()}]).buffer`; + } default: const obj = `{${Object.keys(thing) diff --git a/test/test.js b/test/test.js index afe3557..9dae1ad 100644 --- a/test/test.js +++ b/test/test.js @@ -148,6 +148,18 @@ const fixtures = { value: BigInt('1'), js: '1n', json: '[["BigInt","1"]]' + }, + { + name: 'Uint8Array', + value: new Uint8Array([1, 2, 3]), + js: 'new Uint8Array([1,2,3])', + json: '[["Uint8Array","AQID"]]' + }, + { + name: "ArrayBuffer", + value: new Uint8Array([1, 2, 3]).buffer, + js: 'new Uint8Array([1,2,3]).buffer', + json: '[["ArrayBuffer","AQID"]]' } ], From 46f9bd612f92cfdd33f7b97bcf2a97fc264d99d9 Mon Sep 17 00:00:00 2001 From: Loris Sigrist Date: Sun, 17 Sep 2023 21:07:57 +0200 Subject: [PATCH 2/3] Mention TypedArrays in the features --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9e38635..a4a118b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Like `JSON.stringify`, but handles - dates - `Map` and `Set` - `BigInt` +- `ArrayBuffer` and Typed Arrays - custom types via replacers, reducers and revivers Try it out [here](https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239?version=3.49.0). From 4a2b8cabfb85aa74b7189f4e63d6e668f528f024 Mon Sep 17 00:00:00 2001 From: Loris Sigrist Date: Sun, 17 Sep 2023 21:16:27 +0200 Subject: [PATCH 3/3] remove playground file --- playrgound.js | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 playrgound.js diff --git a/playrgound.js b/playrgound.js deleted file mode 100644 index a943d19..0000000 --- a/playrgound.js +++ /dev/null @@ -1,23 +0,0 @@ -import { stringify } from "./src/stringify.js" -import { parse} from "./src/parse.js" - - -const thing = new Uint8Array(3); -thing[0] = 1; -thing[1] = 2; -thing[2] = 3; - -const otherThing = new Float32Array(10); -otherThing[0] = -Infinity; - - -const a = { - foo: thing, - bar: otherThing, - buff: otherThing.buffer -} - - -const stringified = stringify(a); -const parsed = parse(stringified); -console.log(a, stringified, parsed);