@cloudpss/ubjson
Version:
528 lines (463 loc) • 16.8 kB
text/typescript
/**
* Tests from https://bitbucket.org/shelacek/ubjson
*/
import { jest } from '@jest/globals';
import { Readable, Transform } from 'node:stream';
import { decoder as decodeStream, decode as decodeAsync } from '../../dist/stream/index.js';
import { toBuffer } from '../.utils.ts';
import { UnexpectedEofError as UnexpectedEof } from '../../dist/helper/errors.js';
/**
* 包装为 promise
*/
async function decode(data: Uint8Array<ArrayBuffer>): Promise<unknown> {
const readable = Readable.from([data], { objectMode: false });
return decodeAsync(Readable.toWeb(readable) as ReadableStream<Uint8Array<ArrayBuffer>>);
}
/**
* 包装为 promise
*/
async function eos(stream: Readable): Promise<void> {
return new Promise((resolve, reject) => {
stream.on('error', reject);
stream.on('end', resolve);
});
}
test('decode unsupported type', async () => {
await expect(async () => decode(toBuffer('!'))).rejects.toThrow();
});
test('decode undefined', async () => {
expect(await decode(toBuffer('N'))).toBeUndefined();
});
test('decode undefined (multiple noop)', async () => {
expect(await decode(toBuffer('N', 'N', 'N'))).toBeUndefined();
});
test('decode undefined (empty buffer)', async () => {
expect(await decode(toBuffer())).toBeUndefined();
});
test('decode null', async () => {
// null is not allowed in a stream, will be decoded as undefined
expect(await decode(toBuffer('Z'))).toBeUndefined();
});
test('decode true', async () => {
expect(await decode(toBuffer('T'))).toBe(true);
});
test('decode false', async () => {
expect(await decode(toBuffer('F'))).toBe(false);
});
test('decode int8', async () => {
expect(await decode(toBuffer('i', 100))).toBe(100);
});
test('decode uint8', async () => {
expect(await decode(toBuffer('U', 200))).toBe(200);
});
test('decode int16', async () => {
expect(await decode(toBuffer('I', 0x12, 0x34))).toBe(0x1234);
});
test('decode int32', async () => {
expect(await decode(toBuffer('l', 0x12, 0x34, 0x56, 0x78))).toBe(0x1234_5678);
});
test('decode int64', async () => {
expect(await decode(toBuffer('L', 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0))).toBe(0x1234_5678_9abc_def0n);
expect(await decode(toBuffer('L', 0x00, 0x04, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0))).toBe(0x04_5678_9abc_def0);
});
test('decode float32', async () => {
expect(await decode(toBuffer('d', 0x3f, 0x80, 0x80, 0x00))).toBe(1.003_906_25);
});
test('decode float64', async () => {
expect(await decode(toBuffer('D', 0x40, 0xf8, 0x6a, 0x00, 0x10, 0x00, 0x00, 0x00))).toBe(100_000.003_906_25);
});
test('decode high-precision number [error]', async () => {
await expect(async () => decode(toBuffer('H', 'i', 3, '1', '.', '1'))).rejects.toThrow();
});
test('decode char', async () => {
expect(await decode(toBuffer('C', 'a'))).toBe('a');
});
test('decode string', async () => {
expect(await decode(toBuffer('S', 'i', 6, 'u', 'b', 'j', 's', 'o', 'n'))).toBe('ubjson');
});
test('decode empty string', async () => {
expect(await decode(toBuffer('S', 'i', 0))).toBe('');
});
test('decode string (bad size) [error]', async () => {
await expect(async () => decode(toBuffer('S', 'i', 0xff, 'x'))).rejects.toThrow(/Invalid length/);
});
test('decode string (unexpected eof) [error]', async () => {
await expect(async () => decode(toBuffer('S', 'i', 2, 'x'))).rejects.toThrow(UnexpectedEof);
});
test('decode ascii string', async () => {
const header = toBuffer('S', 'I', 0x3f, 0xff);
// eslint-disable-next-line unicorn/prefer-code-point
const payload = new Uint8Array(0x3fff + header.byteLength).fill('a'.charCodeAt(0));
payload.set(header);
expect(await decode(payload)).toBe('a'.repeat(0x3fff));
});
test('decode ascii string [huge]', async () => {
const header = toBuffer('S', 'I', 0x7f, 0xff);
// eslint-disable-next-line unicorn/prefer-code-point
const payload = new Uint8Array(0x7fff + header.byteLength).fill('a'.charCodeAt(0));
payload.set(header);
expect(await decode(payload)).toBe('a'.repeat(0x7fff));
});
test('decode huge string', async () => {
expect(await decode(toBuffer('S', 'l', 0x00, 0x00, 0x00, 6, 'u', 'b', 'j', 's', 'o', 'n'))).toBe('ubjson');
});
test('decode huge string [int64 length]', async () => {
expect(await decode(toBuffer('S', 'L', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 'x'))).toBe('x');
await expect(async () =>
decode(toBuffer('S', 'L', 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 'x')),
).rejects.toThrow(/Invalid length/);
});
test('decode array', async () => {
expect(await decode(toBuffer('[', 'i', 1, 'i', 2, 'i', 3, ']'))).toEqual([1, 2, 3]);
});
test('decode array (with no-op)', async () => {
expect(await decode(toBuffer('[', 'i', 1, 'N', 'i', 2, 'i', 3, 'N', ']'))).toEqual([1, 2, 3]);
});
test('decode array (empty)', async () => {
expect(await decode(toBuffer('[', ']'))).toEqual([]);
});
test('decode array (empty, optimized)', async () => {
expect(await decode(toBuffer('[', '#', 'i', 0))).toEqual([]);
});
test('decode array (empty, strongly typed, optimized)', async () => {
expect(await decode(toBuffer('[', '$', 'i', '#', 'i', 0))).toEqual(new Int8Array(0));
});
test('decode array (mixed, optimized)', async () => {
expect(await decode(toBuffer('[', '#', 'i', 3, 'i', 1, 'C', 'a', 'T'))).toEqual([1, 'a', true]);
});
test('decode array (strongly typed, optimized)', async () => {
expect(await decode(toBuffer('[', '$', 'i', '#', 'i', 3, 1, 2, 3))).toEqual(new Int8Array([1, 2, 3]));
});
test('decode array (strongly typed, empty, optimized)', async () => {
expect(await decode(toBuffer('[', '$', 'i', '#', 'i', 0))).toEqual(new Int8Array([]));
});
test('decode N-D array (strongly typed, optimized)', async () => {
expect(
await decode(
toBuffer('[', '$', '[', '#', 'i', 2, '$', 'i', '#', 'i', 3, 1, 2, 3, '$', 'i', '#', 'i', 3, 4, 5, 6),
),
).toEqual([new Int8Array([1, 2, 3]), new Int8Array([4, 5, 6])]);
});
test('decode array of objects (optimized)', async () => {
expect(
await decode(
toBuffer(
'[',
'$',
'{',
'#',
'i',
2,
'$',
'i',
'#',
'i',
3,
'i',
1,
'a',
1,
'i',
1,
'b',
2,
'i',
1,
'c',
3,
'$',
'i',
'#',
'i',
3,
'i',
1,
'd',
4,
'i',
1,
'e',
5,
'i',
1,
'f',
6,
),
),
).toEqual([
{ a: 1, b: 2, c: 3 },
{ d: 4, e: 5, f: 6 },
]);
});
test('decode array of objects of arrays (optimized)', async () => {
expect(
await decode(
toBuffer(
'[',
'$',
'{',
'#',
'i',
2,
'$',
'[',
'#',
'i',
2,
'i',
1,
'a',
'$',
'i',
'#',
'i',
2,
1,
2,
'i',
1,
'b',
'$',
'i',
'#',
'i',
2,
3,
4,
'$',
'[',
'#',
'i',
2,
'i',
1,
'c',
'$',
'i',
'#',
'i',
2,
5,
6,
'i',
1,
'd',
'$',
'i',
'#',
'i',
2,
7,
8,
),
),
).toEqual([
{ a: new Int8Array([1, 2]), b: new Int8Array([3, 4]) },
{ c: new Int8Array([5, 6]), d: new Int8Array([7, 8]) },
]);
});
test('decode array (strongly typed, unexpected eof, optimized)', async () => {
await expect(async () => decode(toBuffer('[', '$', 'i', '#', 'i', 3, 1, 2))).rejects.toThrow();
});
test('decode array (strongly typed, invalid length value, optimized)', async () => {
await expect(async () => decode(toBuffer('[', '$', 'i', '#', 'i', -1))).rejects.toThrow();
});
test('decode array (strongly typed, invalid length type, optimized)', async () => {
await expect(async () => decode(toBuffer('[', '$', 'i', '#', 'C', '0'))).rejects.toThrow();
});
test('decode array (strongly typed, malformed, optimized)', async () => {
await expect(async () => decode(toBuffer('[', '$', 'i', 1, 2, 3, ']'))).rejects.toThrow();
});
test('decode array (only null values, optimized)', async () => {
expect(await decode(toBuffer('[', '$', 'Z', '#', 'i', 3))).toEqual([null, null, null]);
});
test('decode array (only true values, optimized)', async () => {
expect(await decode(toBuffer('[', '$', 'T', '#', 'i', 3))).toEqual([true, true, true]);
});
test('decode array (only false values, optimized)', async () => {
expect(await decode(toBuffer('[', '$', 'F', '#', 'i', 3))).toEqual([false, false, false]);
});
test('decode array (int8, strongly typed, optimized) [use typed array]', async () => {
const actual = await decode(toBuffer('[', '$', 'i', '#', 'i', 2, 0x12, 0xfe));
expect(actual).toBeInstanceOf(Int8Array);
expect(actual).toEqual(Int8Array.from([18, -2]));
});
test('decode array (uint8, strongly typed, optimized) [use typed array]', async () => {
const actual = await decode(toBuffer('[', '$', 'U', '#', 'i', 2, 0x12, 0xfe));
expect(actual).toBeInstanceOf(Uint8Array);
expect(actual).toEqual(Uint8Array.from([18, 254]));
});
test('decode array (int16, strongly typed, optimized) [use typed array]', async () => {
const actual = await decode(toBuffer('[', '$', 'I', '#', 'i', 2, 0x12, 0x34, 0xfe, 0xdc));
expect(actual).toBeInstanceOf(Int16Array);
expect(actual).toEqual(Int16Array.from([4660, -292]));
});
test('decode array (int32, strongly typed, optimized) [use typed array]', async () => {
const actual = await decode(toBuffer('[', '$', 'l', '#', 'i', 2, 0x12, 0x34, 0x56, 0x78, 0xfe, 0xdc, 0xba, 0x98));
expect(actual).toBeInstanceOf(Int32Array);
expect(actual).toEqual(Int32Array.from([305_419_896, -19_088_744]));
});
test('decode array (int64, strongly typed, optimized) [use typed array]', async () => {
expect(await decode(toBuffer('[', '$', 'L', '#', 'i', 1, 0x12, 0x34, 0x56, 0x78, 0xfe, 0xdc, 0xba, 0x98))).toEqual(
new BigInt64Array([0x1234_5678_fedc_ba98n]),
);
});
test('decode array (float32, strongly typed, optimized) [use typed array]', async () => {
const actual = await decode(toBuffer('[', '$', 'd', '#', 'i', 2, 0x3e, 0x80, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00));
expect(actual).toBeInstanceOf(Float32Array);
expect(actual).toEqual(Float32Array.from([0.25, 0.125]));
});
test('decode array (float64, strongly typed, optimized) [use typed array]', async () => {
const actual = await decode(
toBuffer(
'[',
'$',
'D',
'#',
'i',
2,
0x3f,
0xd0,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x3f,
0xc0,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
),
);
expect(actual).toBeInstanceOf(Float64Array);
expect(actual).toEqual(Float64Array.from([0.25, 0.125]));
});
test('decode object', async () => {
expect(await decode(toBuffer('{', 'i', 1, 'a', 'i', 1, 'i', 1, 'b', 'i', 2, 'i', 1, 'c', 'i', 3, '}'))).toEqual({
a: 1,
b: 2,
c: 3,
});
});
test('decode object (with no-op)', async () => {
expect(
await decode(
toBuffer('N', '{', 'N', 'i', 1, 'a', 'i', 1, 'i', 1, 'b', 'N', 'i', 2, 'i', 1, 'c', 'i', 3, 'N', '}', 'N'),
),
).toEqual({
a: 1,
b: 2,
c: 3,
});
});
test('decode array (empty, optimized)', async () => {
expect(await decode(toBuffer('{', '#', 'i', 0))).toEqual({});
});
test('decode object (mixed, optimized)', async () => {
expect(
await decode(toBuffer('{', '#', 'i', 3, 'i', 1, 'a', 'i', 1, 'i', 1, 'b', 'C', 'a', 'i', 1, 'c', 'T')),
).toEqual({
a: 1,
b: 'a',
c: true,
});
});
test('decode object (strongly typed, optimized)', async () => {
expect(await decode(toBuffer('{', '$', 'i', '#', 'i', 3, 'i', 1, 'a', 1, 'i', 1, 'b', 2, 'i', 1, 'c', 3))).toEqual({
a: 1,
b: 2,
c: 3,
});
});
test('decode object (only null values, optimized)', async () => {
expect(await decode(toBuffer('{', '$', 'Z', '#', 'i', 3, 'i', 1, 'a', 'i', 1, 'b', 'i', 1, 'c'))).toEqual({
a: null,
b: null,
c: null,
});
});
test('decode object (empty key)', async () => {
expect(await decode(toBuffer('{', 'i', 0, 'T', '}'))).toEqual({
'': true,
});
});
test('decode object (empty key, optimized)', async () => {
expect(await decode(toBuffer('{', '$', 'Z', '#', 'i', 3, 'i', 0, 'i', 1, 'a', 'i', 1, 'b'))).toEqual({
'': null,
a: null,
b: null,
});
});
test('decode stream', async () => {
const stream = Transform.fromWeb(decodeStream() as never, { objectMode: true });
// while decoding streaming, N will be regarded as a no-op, rather than an undefined value
const onData = jest.fn();
stream.on('data', onData);
stream.write(toBuffer('N', 'Z', 'N', 'N', 'T', 'N', 'N', 'F', 'N', 'N'));
stream.write(toBuffer('N', 'N'));
stream.write(toBuffer('I', 0x12));
stream.write(toBuffer(0x34, 'N', 'N', 'N'));
stream.end();
await eos(stream);
expect(onData).toHaveBeenCalledTimes(3);
expect(onData).toHaveBeenNthCalledWith(1, true);
expect(onData).toHaveBeenNthCalledWith(2, false);
expect(onData).toHaveBeenNthCalledWith(3, 0x1234);
});
test('decode bad stream [error]', async () => {
const { readable, writable } = decodeStream();
// while decoding streaming, N will be regarded as a no-op, rather than an undefined value
const onData = jest.fn();
const writer = writable.getWriter();
void writer.write(toBuffer('N', 'Z', 'N', 'N', 'T', 'N', 'N', 'F', 'N', 'N'));
void writer.write(toBuffer('N', 'N'));
void writer.write(toBuffer('I', 0x12));
void writer.write(toBuffer(0x34, '!', 'N'));
void writer.write(toBuffer('I', 0x12, 0x34, 'N'));
void writer.close();
const reader = readable.getReader();
await expect(async () => {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
onData(value);
}
}).rejects.toThrow(/Unexpected marker/);
expect(onData).toHaveBeenCalledTimes(3);
expect(onData).toHaveBeenNthCalledWith(1, true);
expect(onData).toHaveBeenNthCalledWith(2, false);
expect(onData).toHaveBeenNthCalledWith(3, 0x1234);
});
test('decode partial stream [error]', async () => {
const stream = Transform.fromWeb(decodeStream() as never, { objectMode: true });
// while decoding streaming, N will be regarded as a no-op, rather than an undefined value
const onData = jest.fn();
stream.on('data', onData);
stream.write(toBuffer('N', 'Z', 'N', 'N', 'T', 'N', 'N', 'F', 'N', 'N'));
stream.write(toBuffer('N', 'N'));
stream.write(toBuffer('I', 0x12));
stream.end();
await expect(eos(stream)).rejects.toThrow(/Unexpected EOF/);
expect(onData).toHaveBeenCalledTimes(2);
expect(onData).toHaveBeenNthCalledWith(1, true);
expect(onData).toHaveBeenNthCalledWith(2, false);
});
test('decode in parallel', async () => {
const buf = toBuffer('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'S', 'i', 3, ...'str', '}');
const result = await Promise.all([decode(buf), decode(buf)]);
expect(result).toEqual([
{
a: 1,
b: 'str',
},
{
a: 1,
b: 'str',
},
]);
});