seroval
Version:
Stringify JS values
829 lines (771 loc) • 22 kB
text/typescript
import { ALL_ENABLED, Feature } from '../compat';
import {
CONSTANT_VAL,
ERROR_CONSTRUCTOR,
NIL,
SerovalNodeType,
SerovalObjectFlags,
SYMBOL_REF,
} from '../constants';
import {
ARRAY_BUFFER_CONSTRUCTOR,
PROMISE_CONSTRUCTOR,
type PromiseConstructorResolver,
} from '../constructors';
import {
SerovalDepthLimitError,
SerovalDeserializationError,
SerovalMalformedNodeError,
SerovalMissingInstanceError,
SerovalMissingPluginError,
SerovalUnsupportedNodeError,
} from '../errors';
import type { PluginAccessOptions } from '../plugin';
import { SerovalMode } from '../plugin';
import { getReference } from '../reference';
import { createSequence, type Sequence, sequenceToIterator } from '../sequence';
import type { Stream } from '../stream';
import { createStream, isStream, streamToAsyncIterable } from '../stream';
import { deserializeString } from '../string';
import {
SYM_ASYNC_ITERATOR,
SYM_IS_CONCAT_SPREADABLE,
SYM_ITERATOR,
SYM_TO_STRING_TAG,
} from '../symbols';
import type {
SerovalAggregateErrorNode,
SerovalArrayBufferNode,
SerovalArrayNode,
SerovalAsyncIteratorFactoryInstanceNode,
SerovalAsyncIteratorFactoryNode,
SerovalBigIntTypedArrayNode,
SerovalBoxedNode,
SerovalDataViewNode,
SerovalDateNode,
SerovalErrorNode,
SerovalIteratorFactoryInstanceNode,
SerovalIteratorFactoryNode,
SerovalMapNode,
SerovalNode,
SerovalNullConstructorNode,
SerovalObjectNode,
SerovalObjectRecordNode,
SerovalPluginNode,
SerovalPromiseConstructorNode,
SerovalPromiseNode,
SerovalPromiseRejectNode,
SerovalPromiseResolveNode,
SerovalReferenceNode,
SerovalRegExpNode,
SerovalSequenceNode,
SerovalSetNode,
SerovalStreamConstructorNode,
SerovalStreamNextNode,
SerovalStreamReturnNode,
SerovalStreamThrowNode,
SerovalTypedArrayNode,
} from '../types';
import type {
BigIntTypedArrayValue,
TypedArrayValue,
} from '../utils/typed-array';
import { getTypedArrayConstructor } from '../utils/typed-array';
const MAX_BASE64_LENGTH = 1_000_000; // ~0.75MB decoded
const MAX_BIGINT_LENGTH = 10_000;
const MAX_REGEXP_SOURCE_LENGTH = 20_000;
function applyObjectFlag(obj: unknown, flag: SerovalObjectFlags): unknown {
switch (flag) {
case SerovalObjectFlags.Frozen:
return Object.freeze(obj);
case SerovalObjectFlags.NonExtensible:
return Object.preventExtensions(obj);
case SerovalObjectFlags.Sealed:
return Object.seal(obj);
default:
return obj;
}
}
type AssignableValue = AggregateError | Error | Iterable<unknown>;
type AssignableNode = SerovalAggregateErrorNode | SerovalErrorNode;
export interface BaseDeserializerContextOptions extends PluginAccessOptions {
refs?: Map<number, unknown>;
features?: number;
disabledFeatures?: number;
depthLimit?: number;
}
export interface BaseDeserializerContext extends PluginAccessOptions {
readonly mode: SerovalMode;
/**
* Mapping ids to values
*/
refs: Map<number, unknown>;
features: number;
depthLimit: number;
}
const DEFAULT_DEPTH_LIMIT = 1000;
export function createBaseDeserializerContext(
mode: SerovalMode,
options: BaseDeserializerContextOptions,
): BaseDeserializerContext {
return {
mode,
plugins: options.plugins,
refs: options.refs || new Map(),
features: options.features ?? ALL_ENABLED ^ (options.disabledFeatures || 0),
depthLimit: options.depthLimit || DEFAULT_DEPTH_LIMIT,
};
}
export interface VanillaDeserializerContextOptions
extends Omit<BaseDeserializerContextOptions, 'refs'> {
markedRefs: number[] | Set<number>;
}
export interface VanillaDeserializerState {
marked: Set<number>;
}
export interface VanillaDeserializerContext {
mode: SerovalMode.Vanilla;
base: BaseDeserializerContext;
child: DeserializePluginContext | undefined;
state: VanillaDeserializerState;
}
export function createVanillaDeserializerContext(
options: VanillaDeserializerContextOptions,
): VanillaDeserializerContext {
return {
mode: SerovalMode.Vanilla,
base: createBaseDeserializerContext(SerovalMode.Vanilla, options),
child: NIL,
state: {
marked: new Set(options.markedRefs),
},
};
}
export interface CrossDeserializerContext {
mode: SerovalMode.Cross;
base: BaseDeserializerContext;
child: DeserializePluginContext | undefined;
}
export type CrossDeserializerContextOptions = BaseDeserializerContextOptions;
export function createCrossDeserializerContext(
options: CrossDeserializerContextOptions,
): CrossDeserializerContext {
return {
mode: SerovalMode.Cross,
base: createBaseDeserializerContext(SerovalMode.Cross, options),
child: NIL,
};
}
type DeserializerContext =
| VanillaDeserializerContext
| CrossDeserializerContext;
export class DeserializePluginContext {
constructor(
private _p: DeserializerContext,
private depth: number,
) {}
deserialize<T>(node: SerovalNode): T {
return deserialize(this._p, this.depth, node) as T;
}
}
function guardIndexedValue(ctx: BaseDeserializerContext, id: number): void {
if (id < 0 || !Number.isFinite(id) || !Number.isInteger(id)) {
throw new SerovalMalformedNodeError({
t: SerovalNodeType.IndexedValue,
i: id,
} as SerovalNode);
}
if (ctx.refs.has(id)) {
throw new Error('Conflicted ref id: ' + id);
}
}
function assignIndexedValueVanilla<T>(
ctx: VanillaDeserializerContext,
id: number,
value: T,
): T {
guardIndexedValue(ctx.base, id);
if (ctx.state.marked.has(id)) {
ctx.base.refs.set(id, value);
}
return value;
}
function assignIndexedValueCross<T>(
ctx: CrossDeserializerContext,
id: number,
value: T,
): T {
guardIndexedValue(ctx.base, id);
ctx.base.refs.set(id, value);
return value;
}
function assignIndexedValue<T>(
ctx: DeserializerContext,
id: number,
value: T,
): T {
return ctx.mode === SerovalMode.Vanilla
? assignIndexedValueVanilla(ctx, id, value)
: assignIndexedValueCross(ctx, id, value);
}
function deserializeKnownValue<
T extends Record<string, unknown>,
K extends keyof T,
>(node: SerovalNode, record: T, key: K): T[K] {
if (Object.hasOwn(record, key)) {
return record[key];
}
throw new SerovalMalformedNodeError(node);
}
function deserializeReference(
ctx: DeserializerContext,
node: SerovalReferenceNode,
): unknown {
return assignIndexedValue(
ctx,
node.i,
getReference(deserializeString(node.s)),
);
}
function deserializeArray(
ctx: DeserializerContext,
depth: number,
node: SerovalArrayNode,
): unknown[] {
const items = node.a;
const len = items.length;
const result: unknown[] = assignIndexedValue(
ctx,
node.i,
new Array<unknown>(len),
);
for (let i = 0, item: SerovalNode | 0; i < len; i++) {
item = items[i];
if (item) {
result[i] = deserialize(ctx, depth, item);
}
}
applyObjectFlag(result, node.o);
return result;
}
function isValidKey(key: string): boolean {
switch (key) {
case 'constructor':
case '__proto__':
case 'prototype':
case '__defineGetter__':
case '__defineSetter__':
case '__lookupGetter__':
case '__lookupSetter__':
// case 'then':
return false;
default:
return true;
}
}
function isValidSymbol(symbol: symbol): boolean {
switch (symbol) {
case SYM_ASYNC_ITERATOR:
case SYM_IS_CONCAT_SPREADABLE:
case SYM_TO_STRING_TAG:
case SYM_ITERATOR:
return true;
default:
return false;
}
}
function assignStringProperty(
object: Record<string | symbol, unknown>,
key: string,
value: unknown,
): void {
if (isValidKey(key)) {
object[key] = value;
} else {
Object.defineProperty(object, key, {
value,
configurable: true,
enumerable: true,
writable: true,
});
}
}
function assignProperty(
ctx: DeserializerContext,
depth: number,
object: Record<string | symbol, unknown>,
key: string | SerovalNode,
value: SerovalNode,
): void {
if (typeof key === 'string') {
assignStringProperty(object, key, deserialize(ctx, depth, value));
} else {
const actual = deserialize(ctx, depth, key);
switch (typeof actual) {
case 'string':
assignStringProperty(object, actual, deserialize(ctx, depth, value));
break;
case 'symbol':
if (isValidSymbol(actual)) {
object[actual] = deserialize(ctx, depth, value);
}
break;
default:
throw new SerovalMalformedNodeError(key);
}
}
}
function deserializeProperties(
ctx: DeserializerContext,
depth: number,
node: SerovalObjectRecordNode,
result: Record<string | symbol, unknown>,
): Record<string | symbol, unknown> {
const keys = node.k;
const len = keys.length;
if (len > 0) {
for (let i = 0, vals = node.v, len = keys.length; i < len; i++) {
assignProperty(ctx, depth, result, keys[i], vals[i]);
}
}
return result;
}
function deserializeObject(
ctx: DeserializerContext,
depth: number,
node: SerovalObjectNode | SerovalNullConstructorNode,
): Record<string, unknown> {
const result = assignIndexedValue(
ctx,
node.i,
(node.t === SerovalNodeType.Object ? {} : Object.create(null)) as Record<
string,
unknown
>,
);
deserializeProperties(ctx, depth, node.p, result);
applyObjectFlag(result, node.o);
return result;
}
function deserializeDate(
ctx: DeserializerContext,
node: SerovalDateNode,
): Date {
return assignIndexedValue(ctx, node.i, new Date(node.s));
}
function deserializeRegExp(
ctx: DeserializerContext,
node: SerovalRegExpNode,
): RegExp {
if (ctx.base.features & Feature.RegExp) {
const source = deserializeString(node.c);
if (source.length > MAX_REGEXP_SOURCE_LENGTH) {
throw new SerovalMalformedNodeError(node);
}
return assignIndexedValue(ctx, node.i, new RegExp(source, node.m));
}
throw new SerovalUnsupportedNodeError(node);
}
function deserializeSet(
ctx: DeserializerContext,
depth: number,
node: SerovalSetNode,
): Set<unknown> {
const result = assignIndexedValue(ctx, node.i, new Set<unknown>());
for (let i = 0, items = node.a, len = items.length; i < len; i++) {
result.add(deserialize(ctx, depth, items[i]));
}
return result;
}
function deserializeMap(
ctx: DeserializerContext,
depth: number,
node: SerovalMapNode,
): Map<unknown, unknown> {
const result = assignIndexedValue(ctx, node.i, new Map<unknown, unknown>());
for (
let i = 0, keys = node.e.k, vals = node.e.v, len = keys.length;
i < len;
i++
) {
result.set(
deserialize(ctx, depth, keys[i]),
deserialize(ctx, depth, vals[i]),
);
}
return result;
}
function deserializeArrayBuffer(
ctx: DeserializerContext,
node: SerovalArrayBufferNode,
): ArrayBuffer {
if (node.s.length > MAX_BASE64_LENGTH) {
throw new SerovalMalformedNodeError(node);
}
const result = assignIndexedValue(
ctx,
node.i,
ARRAY_BUFFER_CONSTRUCTOR(deserializeString(node.s)),
);
return result;
}
function deserializeTypedArray(
ctx: DeserializerContext,
depth: number,
node: SerovalTypedArrayNode | SerovalBigIntTypedArrayNode,
): TypedArrayValue | BigIntTypedArrayValue {
const construct = getTypedArrayConstructor(node.c) as Int8ArrayConstructor;
const source = deserialize(ctx, depth, node.f) as ArrayBuffer;
const offset = node.b ?? 0;
if (offset < 0 || offset > source.byteLength) {
throw new SerovalMalformedNodeError(node);
}
const result = assignIndexedValue(
ctx,
node.i,
new construct(source, offset, node.l),
);
return result;
}
function deserializeDataView(
ctx: DeserializerContext,
depth: number,
node: SerovalDataViewNode,
): DataView {
const source = deserialize(ctx, depth, node.f) as ArrayBuffer;
const offset = node.b ?? 0;
if (offset < 0 || offset > source.byteLength) {
throw new SerovalMalformedNodeError(node);
}
const result = assignIndexedValue(
ctx,
node.i,
new DataView(source, offset, node.l),
);
return result;
}
function deserializeDictionary<T extends AssignableValue>(
ctx: DeserializerContext,
depth: number,
node: AssignableNode,
result: T,
): T {
if (node.p) {
const fields = deserializeProperties(ctx, depth, node.p, {});
Object.defineProperties(result, Object.getOwnPropertyDescriptors(fields));
}
return result;
}
function deserializeAggregateError(
ctx: DeserializerContext,
depth: number,
node: SerovalAggregateErrorNode,
): AggregateError {
// Serialize the required arguments
const result = assignIndexedValue(
ctx,
node.i,
new AggregateError([], deserializeString(node.m)),
);
// `AggregateError` might've been extended
// either through class or custom properties
// Make sure to assign extra properties
return deserializeDictionary(ctx, depth, node, result);
}
function deserializeError(
ctx: DeserializerContext,
depth: number,
node: SerovalErrorNode,
): Error {
const construct = deserializeKnownValue(node, ERROR_CONSTRUCTOR, node.s);
const result = assignIndexedValue(
ctx,
node.i,
new construct(deserializeString(node.m)),
);
return deserializeDictionary(ctx, depth, node, result);
}
function deserializePromise(
ctx: DeserializerContext,
depth: number,
node: SerovalPromiseNode,
): Promise<unknown> {
const deferred = PROMISE_CONSTRUCTOR();
const result = assignIndexedValue(ctx, node.i, deferred.p);
const deserialized = deserialize(ctx, depth, node.f);
if (node.s) {
deferred.s(deserialized);
} else {
deferred.f(deserialized);
}
return result;
}
function deserializeBoxed(
ctx: DeserializerContext,
depth: number,
node: SerovalBoxedNode,
): unknown {
return assignIndexedValue(
ctx,
node.i,
// biome-ignore lint/style/useConsistentBuiltinInstantiation: intended
Object(deserialize(ctx, depth, node.f)),
);
}
function deserializePlugin(
ctx: DeserializerContext,
depth: number,
node: SerovalPluginNode,
): unknown {
const currentPlugins = ctx.base.plugins;
if (currentPlugins) {
const tag = deserializeString(node.c);
for (let i = 0, len = currentPlugins.length; i < len; i++) {
const plugin = currentPlugins[i];
if (plugin.tag === tag) {
return assignIndexedValue(
ctx,
node.i,
plugin.deserialize(node.s, new DeserializePluginContext(ctx, depth), {
id: node.i,
}),
);
}
}
}
throw new SerovalMissingPluginError(node.c);
}
function deserializePromiseConstructor(
ctx: DeserializerContext,
node: SerovalPromiseConstructorNode,
): unknown {
return assignIndexedValue(
ctx,
node.i,
assignIndexedValue(ctx, node.s, PROMISE_CONSTRUCTOR()).p,
);
}
function deserializePromiseResolve(
ctx: DeserializerContext,
depth: number,
node: SerovalPromiseResolveNode,
): unknown {
const deferred = ctx.base.refs.get(node.i) as
| PromiseConstructorResolver
| undefined;
if (deferred) {
deferred.s(deserialize(ctx, depth, node.a[1]));
return NIL;
}
throw new SerovalMissingInstanceError('Promise');
}
function deserializePromiseReject(
ctx: DeserializerContext,
depth: number,
node: SerovalPromiseRejectNode,
): unknown {
const deferred = ctx.base.refs.get(node.i) as
| PromiseConstructorResolver
| undefined;
if (deferred) {
deferred.f(deserialize(ctx, depth, node.a[1]));
return NIL;
}
throw new SerovalMissingInstanceError('Promise');
}
function deserializeIteratorFactoryInstance(
ctx: DeserializerContext,
depth: number,
node: SerovalIteratorFactoryInstanceNode,
): unknown {
deserialize(ctx, depth, node.a[0]);
const source = deserialize(ctx, depth, node.a[1]);
return sequenceToIterator(source as Sequence);
}
function deserializeAsyncIteratorFactoryInstance(
ctx: DeserializerContext,
depth: number,
node: SerovalAsyncIteratorFactoryInstanceNode,
): unknown {
deserialize(ctx, depth, node.a[0]);
const source = deserialize(ctx, depth, node.a[1]);
return streamToAsyncIterable(source as Stream<any>);
}
function deserializeStreamConstructor(
ctx: DeserializerContext,
depth: number,
node: SerovalStreamConstructorNode,
): unknown {
const result = assignIndexedValue(ctx, node.i, createStream());
const items = node.a;
const len = items.length;
if (len) {
for (let i = 0; i < len; i++) {
deserialize(ctx, depth, items[i]);
}
}
return result;
}
function deserializeStreamNext(
ctx: DeserializerContext,
depth: number,
node: SerovalStreamNextNode,
): unknown {
const deferred = ctx.base.refs.get(node.i) as Stream<unknown> | undefined;
if (deferred && isStream(deferred)) {
deferred.next(deserialize(ctx, depth, node.f));
return NIL;
}
throw new SerovalMissingInstanceError('Stream');
}
function deserializeStreamThrow(
ctx: DeserializerContext,
depth: number,
node: SerovalStreamThrowNode,
): unknown {
const deferred = ctx.base.refs.get(node.i) as Stream<unknown> | undefined;
if (deferred && isStream(deferred)) {
deferred.throw(deserialize(ctx, depth, node.f));
return NIL;
}
throw new SerovalMissingInstanceError('Stream');
}
function deserializeStreamReturn(
ctx: DeserializerContext,
depth: number,
node: SerovalStreamReturnNode,
): unknown {
const deferred = ctx.base.refs.get(node.i) as Stream<unknown> | undefined;
if (deferred && isStream(deferred)) {
deferred.return(deserialize(ctx, depth, node.f));
return NIL;
}
throw new SerovalMissingInstanceError('Stream');
}
function deserializeIteratorFactory(
ctx: DeserializerContext,
depth: number,
node: SerovalIteratorFactoryNode,
): unknown {
deserialize(ctx, depth, node.f);
return NIL;
}
function deserializeAsyncIteratorFactory(
ctx: DeserializerContext,
depth: number,
node: SerovalAsyncIteratorFactoryNode,
): unknown {
deserialize(ctx, depth, node.a[1]);
return NIL;
}
function deserializeSequence(
ctx: DeserializerContext,
depth: number,
node: SerovalSequenceNode,
): Sequence {
const result = assignIndexedValue(
ctx,
node.i,
createSequence([], node.s, node.l),
);
for (let i = 0, len = node.a.length; i < len; i++) {
result.v[i] = deserialize(ctx, depth, node.a[i]);
}
return result;
}
function deserialize(
ctx: DeserializerContext,
depth: number,
node: SerovalNode,
): unknown {
if (depth > ctx.base.depthLimit) {
throw new SerovalDepthLimitError(ctx.base.depthLimit);
}
depth += 1;
switch (node.t) {
case SerovalNodeType.Constant:
return deserializeKnownValue(node, CONSTANT_VAL, node.s);
case SerovalNodeType.Number:
return Number(node.s);
case SerovalNodeType.String:
return deserializeString(String(node.s));
case SerovalNodeType.BigInt:
if (String(node.s).length > MAX_BIGINT_LENGTH) {
throw new SerovalMalformedNodeError(node);
}
return BigInt(node.s);
case SerovalNodeType.IndexedValue:
return ctx.base.refs.get(node.i);
case SerovalNodeType.Reference:
return deserializeReference(ctx, node);
case SerovalNodeType.Array:
return deserializeArray(ctx, depth, node);
case SerovalNodeType.Object:
case SerovalNodeType.NullConstructor:
return deserializeObject(ctx, depth, node);
case SerovalNodeType.Date:
return deserializeDate(ctx, node);
case SerovalNodeType.RegExp:
return deserializeRegExp(ctx, node);
case SerovalNodeType.Set:
return deserializeSet(ctx, depth, node);
case SerovalNodeType.Map:
return deserializeMap(ctx, depth, node);
case SerovalNodeType.ArrayBuffer:
return deserializeArrayBuffer(ctx, node);
case SerovalNodeType.BigIntTypedArray:
case SerovalNodeType.TypedArray:
return deserializeTypedArray(ctx, depth, node);
case SerovalNodeType.DataView:
return deserializeDataView(ctx, depth, node);
case SerovalNodeType.AggregateError:
return deserializeAggregateError(ctx, depth, node);
case SerovalNodeType.Error:
return deserializeError(ctx, depth, node);
case SerovalNodeType.Promise:
return deserializePromise(ctx, depth, node);
case SerovalNodeType.WKSymbol:
return deserializeKnownValue(node, SYMBOL_REF, node.s);
case SerovalNodeType.Boxed:
return deserializeBoxed(ctx, depth, node);
case SerovalNodeType.Plugin:
return deserializePlugin(ctx, depth, node);
case SerovalNodeType.PromiseConstructor:
return deserializePromiseConstructor(ctx, node);
case SerovalNodeType.PromiseSuccess:
return deserializePromiseResolve(ctx, depth, node);
case SerovalNodeType.PromiseFailure:
return deserializePromiseReject(ctx, depth, node);
case SerovalNodeType.IteratorFactoryInstance:
return deserializeIteratorFactoryInstance(ctx, depth, node);
case SerovalNodeType.AsyncIteratorFactoryInstance:
return deserializeAsyncIteratorFactoryInstance(ctx, depth, node);
case SerovalNodeType.StreamConstructor:
return deserializeStreamConstructor(ctx, depth, node);
case SerovalNodeType.StreamNext:
return deserializeStreamNext(ctx, depth, node);
case SerovalNodeType.StreamThrow:
return deserializeStreamThrow(ctx, depth, node);
case SerovalNodeType.StreamReturn:
return deserializeStreamReturn(ctx, depth, node);
case SerovalNodeType.IteratorFactory:
return deserializeIteratorFactory(ctx, depth, node);
case SerovalNodeType.AsyncIteratorFactory:
return deserializeAsyncIteratorFactory(ctx, depth, node);
// case SerovalNodeType.SpecialReference:
case SerovalNodeType.Sequence:
return deserializeSequence(ctx, depth, node);
default:
throw new SerovalUnsupportedNodeError(node);
}
}
export function deserializeTop(
ctx: DeserializerContext,
node: SerovalNode,
): unknown {
try {
return deserialize(ctx, 0, node);
} catch (error) {
throw new SerovalDeserializationError(error);
}
}