Skip to content

Commit

Permalink
make encode() and decode() re-entrant
Browse files Browse the repository at this point in the history
  • Loading branch information
gfx committed Feb 6, 2025
1 parent b41ccd7 commit 70c21a9
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 3 deletions.
60 changes: 59 additions & 1 deletion src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ export class Decoder<ContextType = undefined> {
private headByte = HEAD_BYTE_REQUIRED;
private readonly stack = new StackPool();

private entered = false;

public constructor(options?: DecoderOptions<ContextType>) {
this.extensionCodec = options?.extensionCodec ?? (ExtensionCodec.defaultCodec as ExtensionCodecType<ContextType>);
this.context = (options as { context: ContextType } | undefined)?.context as ContextType; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
Expand All @@ -235,6 +237,22 @@ export class Decoder<ContextType = undefined> {
this.keyDecoder = options?.keyDecoder !== undefined ? options.keyDecoder : sharedCachedKeyDecoder;
}

private clone(): Decoder<ContextType> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return new Decoder({
extensionCodec: this.extensionCodec,
context: this.context,
useBigInt64: this.useBigInt64,
rawStrings: this.rawStrings,
maxStrLength: this.maxStrLength,
maxBinLength: this.maxBinLength,
maxArrayLength: this.maxArrayLength,
maxMapLength: this.maxMapLength,
maxExtLength: this.maxExtLength,
keyDecoder: this.keyDecoder,
} as any);
}

private reinitializeState() {
this.totalPos = 0;
this.headByte = HEAD_BYTE_REQUIRED;
Expand Down Expand Up @@ -274,11 +292,27 @@ export class Decoder<ContextType = undefined> {
return new RangeError(`Extra ${view.byteLength - pos} of ${view.byteLength} byte(s) found at buffer[${posToShow}]`);
}

private enteringGuard(): Disposable {
this.entered = true;
return {
[Symbol.dispose]: () => {
this.entered = false;
},
};
}

/**
* @throws {@link DecodeError}
* @throws {@link RangeError}
*/
public decode(buffer: ArrayLike<number> | ArrayBufferView | ArrayBufferLike): unknown {
if (this.entered) {
const instance = this.clone();
return instance.decode(buffer);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using _guard = this.enteringGuard();

this.reinitializeState();
this.setBuffer(buffer);

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

public *decodeMulti(buffer: ArrayLike<number> | ArrayBufferView | ArrayBufferLike): Generator<unknown, void, unknown> {
if (this.entered) {
const instance = this.clone();
yield* instance.decodeMulti(buffer);

Check failure on line 329 in src/Decoder.ts

View workflow job for this annotation

GitHub Actions / fuzzing

Type 'Generator<unknown, void, unknown>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.

Check failure on line 329 in src/Decoder.ts

View workflow job for this annotation

GitHub Actions / browser (ChromeHeadless)

Type 'Generator<unknown, void, unknown>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.

Check failure on line 329 in src/Decoder.ts

View workflow job for this annotation

GitHub Actions / browser (FirefoxHeadless)

Type 'Generator<unknown, void, unknown>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.

Check failure on line 329 in src/Decoder.ts

View workflow job for this annotation

GitHub Actions / nodejs (18)

Type 'Generator<unknown, void, unknown>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.

Check failure on line 329 in src/Decoder.ts

View workflow job for this annotation

GitHub Actions / nodejs (20)

Type 'Generator<unknown, void, unknown>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.

Check failure on line 329 in src/Decoder.ts

View workflow job for this annotation

GitHub Actions / nodejs (22)

Type 'Generator<unknown, void, unknown>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using _guard = this.enteringGuard();

this.reinitializeState();
this.setBuffer(buffer);

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

public async decodeAsync(stream: AsyncIterable<ArrayLike<number> | ArrayBufferView | ArrayBufferLike>): Promise<unknown> {
if (this.entered) {
const instance = this.clone();
return instance.decodeAsync(stream);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using _guard = this.enteringGuard();

let decoded = false;
let object: unknown;
for await (const buffer of stream) {
if (decoded) {
this.entered = false;
throw this.createExtraByteError(this.totalPos);
}

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

private async *decodeMultiAsync(stream: AsyncIterable<ArrayLike<number> | ArrayBufferView | ArrayBufferLike>, isArray: boolean) {
private async *decodeMultiAsync(stream: AsyncIterable<ArrayLike<number> | ArrayBufferView | ArrayBufferLike>, isArray: boolean): AsyncGenerator<unknown, void, unknown> {
if (this.entered) {
const instance = this.clone();
yield* instance.decodeMultiAsync(stream, isArray);
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using _guard = this.enteringGuard();

let isArrayHeaderRequired = isArray;
let arrayItemsLeft = -1;

Expand Down
42 changes: 42 additions & 0 deletions src/Encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export class Encoder<ContextType = undefined> {
private view: DataView;
private bytes: Uint8Array;

private entered = false;

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

private clone() {
// Because of slightly special argument `context`,
// type assertion is needed.
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return new Encoder<ContextType>({
extensionCodec: this.extensionCodec,
context: this.context,
useBigInt64: this.useBigInt64,
maxDepth: this.maxDepth,
initialBufferSize: this.initialBufferSize,
sortKeys: this.sortKeys,
forceFloat32: this.forceFloat32,
ignoreUndefined: this.ignoreUndefined,
forceIntegerToFloat: this.forceIntegerToFloat,
} as any);
}

private reinitializeState() {
this.pos = 0;
}

private enteringGuard(): Disposable {
this.entered = true;
return {
[Symbol.dispose]: () => {
this.entered = false;
},
};
}

/**
* 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}.
*
* @returns Encodes the object and returns a shared reference the encoder's internal buffer.
*/
public encodeSharedRef(object: unknown): Uint8Array {
if (this.entered) {
const instance = this.clone();
return instance.encodeSharedRef(object);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using _guard = this.enteringGuard();

this.reinitializeState();
this.doEncode(object, 1);
return this.bytes.subarray(0, this.pos);
Expand All @@ -122,6 +157,13 @@ export class Encoder<ContextType = undefined> {
* @returns Encodes the object and returns a copy of the encoder's internal buffer.
*/
public encode(object: unknown): Uint8Array {
if (this.entered) {
const instance = this.clone();
return instance.encode(object);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using _guard = this.enteringGuard();

this.reinitializeState();
this.doEncode(object, 1);
return this.bytes.slice(0, this.pos);
Expand Down
11 changes: 9 additions & 2 deletions test/reuse-instances-with-extensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class MsgPackContext {
readonly extensionCodec = new ExtensionCodec<MsgPackContext>();

constructor() {
const encoder = new Encoder<MsgPackContext>({ extensionCodec: this.extensionCodec, context: this });
const decoder = new Decoder<MsgPackContext>({ extensionCodec: this.extensionCodec, context: this });
const encoder = new Encoder({ extensionCodec: this.extensionCodec, context: this });
const decoder = new Decoder({ extensionCodec: this.extensionCodec, context: this });

this.encode = encoder.encode.bind(encoder);
this.decode = decoder.decode.bind(decoder);
Expand All @@ -38,4 +38,11 @@ describe("reuse instances with extensions", () => {
const data = context.decode(buf);
deepStrictEqual(data, BigInt(42));
});

it("should encode and decode bigints", () => {
const context = new MsgPackContext();
const buf = context.encode([BigInt(1), BigInt(2), BigInt(3)]);
const data = context.decode(buf);
deepStrictEqual(data, [BigInt(1), BigInt(2), BigInt(3)]);
});
});

0 comments on commit 70c21a9

Please sign in to comment.