Skip to content

Commit 77551ed

Browse files
committed
feat: 🎸 Clearly distinguish between ArrayBuffer and TypedArray
1 parent 8f31103 commit 77551ed

5 files changed

Lines changed: 84 additions & 7 deletions

File tree

README.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,41 @@ const restoredUnsafe = jsonWeb3.parse_UNSAFE(textUnsafe)
6565
- `stringify_UNSAFE(value, replacer?, space?)` (serializes `Function` payloads)
6666
- `parse_UNSAFE(text, reviver?)` (revives `Function` payloads via `new Function(...)`)
6767

68+
## Type support
69+
70+
| type | supported by standard JSON? | supported by json-web3? |
71+
| ------------------- | --------------------------- | ------------------------------------- |
72+
| `string` |||
73+
| `number` |||
74+
| `boolean` |||
75+
| `null` |||
76+
| `Array` |||
77+
| `Object` |||
78+
| `undefined` |||
79+
| `Infinity` |||
80+
| `-Infinity` |||
81+
| `NaN` |||
82+
| `BigInt` |||
83+
| `Date` |||
84+
| `RegExp` |||
85+
| `Set` |||
86+
| `Map` |||
87+
| `URL` |||
88+
| `ArrayBuffer` |||
89+
| `Uint8Array` |||
90+
| `Uint8ClampedArray` |||
91+
| `Uint16Array` |||
92+
| `Uint32Array` |||
93+
| `Int8Array` |||
94+
| `Int16Array` |||
95+
| `Int32Array` |||
96+
| `Float16Array` |||
97+
| `Float32Array` |||
98+
| `Float64Array` |||
99+
| `BigInt64Array` |||
100+
| `BigUint64Array` |||
101+
| `Function` || ✅ ⚠️(use UNSAFE api, it's dangerous) |
102+
68103
## Note
69104

70105
- `bigint` values are encoded as objects: `{"__@json.bigint__":"<value>"}`.
@@ -73,7 +108,8 @@ const restoredUnsafe = jsonWeb3.parse_UNSAFE(textUnsafe)
73108
- `Map` values are encoded as `{"__@json.map__":[[k,v],...]}` and `Set` values as `{"__@json.set__":[...]}`.
74109
- `RegExp` values are encoded as `{"__@json.regexp__":{"source":"...","flags":"..."}}`.
75110
- `URL` values are encoded as `{"__@json.url__":"..."}`.
76-
- `Function` values are encoded as `{"__@json.function__":"<source>"}` by `stringify_UNSAFE` and are only revived by `parse_UNSAFE` using `new Function(...)` (only do this with trusted input).
77-
- `ArrayBuffer`, Node `Buffer` JSON shapes, and typed arrays are encoded as `{"__@json.typedarray__":{"type":"<Name>","bytes":"0x..."}}` and decoded back to the original typed array (`Uint8Array`, `Uint8ClampedArray`, `Uint16Array`, `Uint32Array`, `Int8Array`, `Int16Array`, `Int32Array`, `Float32Array`, `Float64Array`, `BigInt64Array`, `BigUint64Array`).
111+
- `Function` values are encoded as `{"__@json.function__":"<source>"}` by `stringify_UNSAFE` and are only revived by `parse_UNSAFE` using `new Function(...)` (using the UNSAFE function pair is dangerous; make sure your data is trusted).
112+
- `ArrayBuffer` values are encoded as `{"__@json.arraybuffer__":{"bytes":"0x..."}}` and decoded back to `ArrayBuffer`.
113+
- Node `Buffer` JSON shapes and typed arrays are encoded as `{"__@json.typedarray__":{"type":"<Name>","bytes":"0x..."}}` and decoded back to the original typed array (`Uint8Array`, `Uint8ClampedArray`, `Uint16Array`, `Uint32Array`, `Int8Array`, `Int16Array`, `Int32Array`, `Float32Array`, `Float64Array`, `BigInt64Array`, `BigUint64Array`).
78114

79115
Compared to libraries that require eval-based parsing (for example, `serialize-javascript`), this approach is generally safer and more efficient.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-web3",
3-
"version": "1.2.2",
3+
"version": "1.3.0",
44
"description": "BigInt-safe JSON serialization and deserialization for Web3 use cases.",
55
"keywords": [
66
"json",

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
2+
ARRAYBUFFER_TAG,
23
BIGINT_TAG,
34
DATE_TAG,
45
fromSerializable,
56
FUNCTION_TAG,
67
isArray,
8+
isArrayBufferPayload,
79
isDate,
810
isFunction,
911
isRegExpPayload,
@@ -38,7 +40,9 @@ const applyReplacer = (holder: any, key: string, value: any, replacer: Replacer)
3840
if (key === URL_TAG) return value
3941
if (key === FUNCTION_TAG) return value
4042
if (key === TYPEDARRAY_TAG) return value
43+
if (key === ARRAYBUFFER_TAG) return value
4144
if (isTypedArrayPayload(holder)) return value
45+
if (isArrayBufferPayload(holder)) return value
4246
if (isRegExpPayload(holder)) return value
4347
if (isArray(holder)) return value
4448
return replacer.includes(key) ? value : undefined

src/utils.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const BIGINT_TAG = '__@json.bigint__'
22
export const TYPEDARRAY_TAG = '__@json.typedarray__'
3+
export const ARRAYBUFFER_TAG = '__@json.arraybuffer__'
34
export const DATE_TAG = '__@json.date__'
45
export const MAP_TAG = '__@json.map__'
56
export const SET_TAG = '__@json.set__'
@@ -141,7 +142,7 @@ export const toSerializable = (value: any, options: { allowFunction?: boolean }
141142
}
142143

143144
if (isArrayBuffer(value)) {
144-
return { [TYPEDARRAY_TAG]: { type: 'Uint8Array', bytes: toHex(new Uint8Array(value)) } }
145+
return { [ARRAYBUFFER_TAG]: { bytes: toHex(new Uint8Array(value)) } }
145146
}
146147

147148
if (
@@ -223,6 +224,16 @@ export const fromSerializable = (value: any, options: { allowFunction?: boolean
223224
new Uint8Array(buffer).set(bytes)
224225
return new ctor(buffer)
225226
}
227+
if (hasOwnProperty(value, ARRAYBUFFER_TAG)) {
228+
const payload = value[ARRAYBUFFER_TAG]
229+
if (!payload || !isObject(payload) || !isString(payload.bytes)) {
230+
throw new Error('Invalid arraybuffer payload')
231+
}
232+
const bytes = fromHex(payload.bytes)
233+
const buffer = new ArrayBuffer(bytes.byteLength)
234+
new Uint8Array(buffer).set(bytes)
235+
return buffer
236+
}
226237
if (
227238
value.type === 'Buffer' &&
228239
isArray(value.data) &&
@@ -241,6 +252,9 @@ export const isTypedArrayPayload = (value: any): boolean =>
241252
isString(value.bytes) &&
242253
Object.keys(value).length === 2
243254

255+
export const isArrayBufferPayload = (value: any): boolean =>
256+
isObject(value) && isString(value.bytes) && Object.keys(value).length === 1
257+
244258
export const isRegExpPayload = (value: any): boolean =>
245259
isObject(value) &&
246260
isString(value.source) &&

test/index.test.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,15 @@ describe('json-web3', () => {
119119
expect(Array.from(output)).toEqual([10, 11, 12])
120120
})
121121

122-
it('handles ArrayBuffer value as bytes', () => {
122+
it('handles ArrayBuffer value as ArrayBuffer', () => {
123123
const ab = new ArrayBuffer(4)
124124
new Uint8Array(ab).set([9, 8, 7, 6])
125125

126126
const text = stringify({ ab })
127127
const output = parse(text)
128128

129-
expect(output.ab).toBeInstanceOf(Uint8Array)
130-
expect(Array.from(output.ab)).toEqual([9, 8, 7, 6])
129+
expect(output.ab).toBeInstanceOf(ArrayBuffer)
130+
expect(Array.from(new Uint8Array(output.ab))).toEqual([9, 8, 7, 6])
131131
})
132132

133133
it('does not mutate JSON primitives and null', () => {
@@ -226,6 +226,15 @@ describe('json-web3', () => {
226226
expect(Array.from(output.data)).toEqual([1])
227227
})
228228

229+
it('replacer array does not drop arraybuffer tag keys', () => {
230+
const input = { data: { '__@json.arraybuffer__': { bytes: '0x0102' } } }
231+
const text = stringify(input, ['data'])
232+
const output = parse(text)
233+
234+
expect(output.data).toBeInstanceOf(ArrayBuffer)
235+
expect(Array.from(new Uint8Array(output.data))).toEqual([1, 2])
236+
})
237+
229238
it('replacer array does not drop new internal tag keys', () => {
230239
const input = {
231240
d: new Date('2020-01-02T03:04:05.006Z'),
@@ -298,6 +307,11 @@ describe('json-web3', () => {
298307
expect(() => parse_UNSAFE(text)).toThrowError()
299308
})
300309

310+
it('parse throws on invalid arraybuffer payload', () => {
311+
const text = '{"x":{"__@json.arraybuffer__":{"bytes":123}}}'
312+
expect(() => parse(text)).toThrowError()
313+
})
314+
301315
it('falls back to raw bytes when typed array type is unknown', () => {
302316
const text = '{"__@json.typedarray__":{"type":"UnknownArray","bytes":"0x01"}}'
303317
const output = parse(text)
@@ -441,6 +455,15 @@ describe('json-web3', () => {
441455
expect(output).toEqual({})
442456
})
443457

458+
it('parse_UNSAFE reviver receives decoded values', () => {
459+
const text = stringify_UNSAFE({ fn: (arg: string) => arg })
460+
const output = parse_UNSAFE(text, (_key, value) =>
461+
typeof value === 'function' ? value('ok') : value,
462+
)
463+
464+
expect(output.fn).toBe('ok')
465+
})
466+
444467
it('BigInt in array with replacer array filtering works as expected', () => {
445468
const input = { list: [1n, 2n], other: 3n }
446469
const text = stringify(input, ['list'])

0 commit comments

Comments
 (0)