Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for ArrayBuffer and the various TypedArrays #69

Merged
merged 4 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
110 changes: 110 additions & 0 deletions src/base64.js
Original file line number Diff line number Diff line change
@@ -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;
}
27 changes: 27 additions & 0 deletions src/parse.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { decode64 } from './base64.js';
import {
HOLE,
NAN,
Expand Down Expand Up @@ -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}`);
}
Expand Down
28 changes: 28 additions & 0 deletions src/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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`
Expand Down Expand Up @@ -136,6 +137,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(
Expand Down
1 change: 1 addition & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
37 changes: 37 additions & 0 deletions src/uneval.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,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)) {
Expand Down Expand Up @@ -160,6 +176,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)
Expand Down
12 changes: 12 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,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"]]'
}
],

Expand Down
Loading