Skip to content

Commit

Permalink
Merge pull request #69 from LorisSigrist/master
Browse files Browse the repository at this point in the history
Add Support for ArrayBuffer and the various TypedArrays
  • Loading branch information
Rich-Harris authored Sep 25, 2024
2 parents bd31380 + a4fe876 commit 0bbcc96
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 0 deletions.
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 @@ -15,6 +15,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 @@ -137,6 +138,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 @@ -82,6 +82,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 @@ -161,6 +177,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

0 comments on commit 0bbcc96

Please sign in to comment.