Skip to content

Commit 70c21a9

Browse files
committed
make encode() and decode() re-entrant
1 parent b41ccd7 commit 70c21a9

File tree

3 files changed

+110
-3
lines changed

3 files changed

+110
-3
lines changed

src/Decoder.ts

+59-1
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ export class Decoder<ContextType = undefined> {
221221
private headByte = HEAD_BYTE_REQUIRED;
222222
private readonly stack = new StackPool();
223223

224+
private entered = false;
225+
224226
public constructor(options?: DecoderOptions<ContextType>) {
225227
this.extensionCodec = options?.extensionCodec ?? (ExtensionCodec.defaultCodec as ExtensionCodecType<ContextType>);
226228
this.context = (options as { context: ContextType } | undefined)?.context as ContextType; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
@@ -235,6 +237,22 @@ export class Decoder<ContextType = undefined> {
235237
this.keyDecoder = options?.keyDecoder !== undefined ? options.keyDecoder : sharedCachedKeyDecoder;
236238
}
237239

240+
private clone(): Decoder<ContextType> {
241+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
242+
return new Decoder({
243+
extensionCodec: this.extensionCodec,
244+
context: this.context,
245+
useBigInt64: this.useBigInt64,
246+
rawStrings: this.rawStrings,
247+
maxStrLength: this.maxStrLength,
248+
maxBinLength: this.maxBinLength,
249+
maxArrayLength: this.maxArrayLength,
250+
maxMapLength: this.maxMapLength,
251+
maxExtLength: this.maxExtLength,
252+
keyDecoder: this.keyDecoder,
253+
} as any);
254+
}
255+
238256
private reinitializeState() {
239257
this.totalPos = 0;
240258
this.headByte = HEAD_BYTE_REQUIRED;
@@ -274,11 +292,27 @@ export class Decoder<ContextType = undefined> {
274292
return new RangeError(`Extra ${view.byteLength - pos} of ${view.byteLength} byte(s) found at buffer[${posToShow}]`);
275293
}
276294

295+
private enteringGuard(): Disposable {
296+
this.entered = true;
297+
return {
298+
[Symbol.dispose]: () => {
299+
this.entered = false;
300+
},
301+
};
302+
}
303+
277304
/**
278305
* @throws {@link DecodeError}
279306
* @throws {@link RangeError}
280307
*/
281308
public decode(buffer: ArrayLike<number> | ArrayBufferView | ArrayBufferLike): unknown {
309+
if (this.entered) {
310+
const instance = this.clone();
311+
return instance.decode(buffer);
312+
}
313+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
314+
using _guard = this.enteringGuard();
315+
282316
this.reinitializeState();
283317
this.setBuffer(buffer);
284318

@@ -290,6 +324,14 @@ export class Decoder<ContextType = undefined> {
290324
}
291325

292326
public *decodeMulti(buffer: ArrayLike<number> | ArrayBufferView | ArrayBufferLike): Generator<unknown, void, unknown> {
327+
if (this.entered) {
328+
const instance = this.clone();
329+
yield* instance.decodeMulti(buffer);
330+
return;
331+
}
332+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
333+
using _guard = this.enteringGuard();
334+
293335
this.reinitializeState();
294336
this.setBuffer(buffer);
295337

@@ -299,10 +341,18 @@ export class Decoder<ContextType = undefined> {
299341
}
300342

301343
public async decodeAsync(stream: AsyncIterable<ArrayLike<number> | ArrayBufferView | ArrayBufferLike>): Promise<unknown> {
344+
if (this.entered) {
345+
const instance = this.clone();
346+
return instance.decodeAsync(stream);
347+
}
348+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
349+
using _guard = this.enteringGuard();
350+
302351
let decoded = false;
303352
let object: unknown;
304353
for await (const buffer of stream) {
305354
if (decoded) {
355+
this.entered = false;
306356
throw this.createExtraByteError(this.totalPos);
307357
}
308358

@@ -343,7 +393,15 @@ export class Decoder<ContextType = undefined> {
343393
return this.decodeMultiAsync(stream, false);
344394
}
345395

346-
private async *decodeMultiAsync(stream: AsyncIterable<ArrayLike<number> | ArrayBufferView | ArrayBufferLike>, isArray: boolean) {
396+
private async *decodeMultiAsync(stream: AsyncIterable<ArrayLike<number> | ArrayBufferView | ArrayBufferLike>, isArray: boolean): AsyncGenerator<unknown, void, unknown> {
397+
if (this.entered) {
398+
const instance = this.clone();
399+
yield* instance.decodeMultiAsync(stream, isArray);
400+
return;
401+
}
402+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
403+
using _guard = this.enteringGuard();
404+
347405
let isArrayHeaderRequired = isArray;
348406
let arrayItemsLeft = -1;
349407

src/Encoder.ts

+42
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export class Encoder<ContextType = undefined> {
8686
private view: DataView;
8787
private bytes: Uint8Array;
8888

89+
private entered = false;
90+
8991
public constructor(options?: EncoderOptions<ContextType>) {
9092
this.extensionCodec = options?.extensionCodec ?? (ExtensionCodec.defaultCodec as ExtensionCodecType<ContextType>);
9193
this.context = (options as { context: ContextType } | undefined)?.context as ContextType; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
@@ -103,16 +105,49 @@ export class Encoder<ContextType = undefined> {
103105
this.bytes = new Uint8Array(this.view.buffer);
104106
}
105107

108+
private clone() {
109+
// Because of slightly special argument `context`,
110+
// type assertion is needed.
111+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
112+
return new Encoder<ContextType>({
113+
extensionCodec: this.extensionCodec,
114+
context: this.context,
115+
useBigInt64: this.useBigInt64,
116+
maxDepth: this.maxDepth,
117+
initialBufferSize: this.initialBufferSize,
118+
sortKeys: this.sortKeys,
119+
forceFloat32: this.forceFloat32,
120+
ignoreUndefined: this.ignoreUndefined,
121+
forceIntegerToFloat: this.forceIntegerToFloat,
122+
} as any);
123+
}
124+
106125
private reinitializeState() {
107126
this.pos = 0;
108127
}
109128

129+
private enteringGuard(): Disposable {
130+
this.entered = true;
131+
return {
132+
[Symbol.dispose]: () => {
133+
this.entered = false;
134+
},
135+
};
136+
}
137+
110138
/**
111139
* This is almost equivalent to {@link Encoder#encode}, but it returns an reference of the encoder's internal buffer and thus much faster than {@link Encoder#encode}.
112140
*
113141
* @returns Encodes the object and returns a shared reference the encoder's internal buffer.
114142
*/
115143
public encodeSharedRef(object: unknown): Uint8Array {
144+
if (this.entered) {
145+
const instance = this.clone();
146+
return instance.encodeSharedRef(object);
147+
}
148+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
149+
using _guard = this.enteringGuard();
150+
116151
this.reinitializeState();
117152
this.doEncode(object, 1);
118153
return this.bytes.subarray(0, this.pos);
@@ -122,6 +157,13 @@ export class Encoder<ContextType = undefined> {
122157
* @returns Encodes the object and returns a copy of the encoder's internal buffer.
123158
*/
124159
public encode(object: unknown): Uint8Array {
160+
if (this.entered) {
161+
const instance = this.clone();
162+
return instance.encode(object);
163+
}
164+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
165+
using _guard = this.enteringGuard();
166+
125167
this.reinitializeState();
126168
this.doEncode(object, 1);
127169
return this.bytes.slice(0, this.pos);

test/reuse-instances-with-extensions.test.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ class MsgPackContext {
2121
readonly extensionCodec = new ExtensionCodec<MsgPackContext>();
2222

2323
constructor() {
24-
const encoder = new Encoder<MsgPackContext>({ extensionCodec: this.extensionCodec, context: this });
25-
const decoder = new Decoder<MsgPackContext>({ extensionCodec: this.extensionCodec, context: this });
24+
const encoder = new Encoder({ extensionCodec: this.extensionCodec, context: this });
25+
const decoder = new Decoder({ extensionCodec: this.extensionCodec, context: this });
2626

2727
this.encode = encoder.encode.bind(encoder);
2828
this.decode = decoder.decode.bind(decoder);
@@ -38,4 +38,11 @@ describe("reuse instances with extensions", () => {
3838
const data = context.decode(buf);
3939
deepStrictEqual(data, BigInt(42));
4040
});
41+
42+
it("should encode and decode bigints", () => {
43+
const context = new MsgPackContext();
44+
const buf = context.encode([BigInt(1), BigInt(2), BigInt(3)]);
45+
const data = context.decode(buf);
46+
deepStrictEqual(data, [BigInt(1), BigInt(2), BigInt(3)]);
47+
});
4148
});

0 commit comments

Comments
 (0)