Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,33 @@ npm install @msgpack/msgpack

## API

### `encode(data: unknown, options?): Uint8Array`
### `encode(data: unknown, options?: EncodeOptions): Uint8Array`

It encodes `data` and returns a byte array as `Uint8Array`.

### `decode(buffer: ArrayLike<number> | Uint8Array, options?): unknown`
### `decode(buffer: ArrayLike<number> | Uint8Array, options?: DecodeOptions): unknown`

It decodes `buffer` in a byte buffer and returns decoded data as `uknown`.

### `decodeAsync(stream: AsyncIterable<ArrayLike<number> | Uint8Array>, options?): Promise<unknown>`
#### DecodeOptions

Name|Type|Default
----|----|----
extensionCodec | ExtensionCodec | `ExtensinCodec.defaultCodec`
maxStrLength | number | `4_294_967_295` (UINT32_MAX)
maxBinLength | number | `4_294_967_295` (UINT32_MAX)
maxArrayLength | number | `4_294_967_295` (UINT32_MAX)
maxMapLength | number | `4_294_967_295` (UINT32_MAX)
maxExtLength | number | `4_294_967_295` (UINT32_MAX)

You can use `max${Type}Length` to limit the length of each type decoded.

### `decodeAsync(stream: AsyncIterable<ArrayLike<number> | Uint8Array>, options?: DecodeAsyncOptions): Promise<unknown>`

It decodes `stream` in an async iterable of byte arrays and returns decoded data as `uknown` wrapped in `Promise`. This function works asyncronously.

Note that `decodeAsync()` acceps the same options as `decode()`.

### Extension Types

To handle [MessagePack Extension Types](https://github.com/msgpack/msgpack/blob/master/spec.md#extension-types), this library provides `ExtensionCodec` class.
Expand Down
37 changes: 36 additions & 1 deletion src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const DataViewIndexOutOfBoundsError: typeof Error = (() => {

const MORE_DATA = new DataViewIndexOutOfBoundsError("Insufficient data");

const DEFAULT_MAX_LENGTH = 0xffff_ffff; // uint32_max

export class Decoder {
totalPos = 0;
pos = 0;
Expand All @@ -53,7 +55,14 @@ export class Decoder {
headByte = HEAD_BYTE_REQUIRED;
readonly stack: Array<StackState> = [];

constructor(readonly extensionCodec = ExtensionCodec.defaultCodec) {}
constructor(
readonly extensionCodec = ExtensionCodec.defaultCodec,
readonly maxStrLength = DEFAULT_MAX_LENGTH,
readonly maxBinLength = DEFAULT_MAX_LENGTH,
readonly maxArrayLength = DEFAULT_MAX_LENGTH,
readonly maxMapLength = DEFAULT_MAX_LENGTH,
readonly maxExtLength = DEFAULT_MAX_LENGTH,
) {}

setBuffer(buffer: ArrayLike<number> | Uint8Array): void {
this.view = createDataView(buffer);
Expand Down Expand Up @@ -356,6 +365,12 @@ export class Decoder {
}

pushMapState(size: number) {
if (size > this.maxMapLength) {
throw new Error(
`Max length exceeded: map length (${size}) > maxMapLengthLength (${this.maxMapLength})`,
);
}

this.stack.push({
type: State.MAP_KEY,
size,
Expand All @@ -366,6 +381,12 @@ export class Decoder {
}

pushArrayState(size: number) {
if (size > this.maxArrayLength) {
throw new Error(
`Max length exceeded: array length (${size}) > maxArrayLength (${this.maxArrayLength})`,
);
}

this.stack.push({
type: State.ARRAY,
size,
Expand All @@ -374,12 +395,22 @@ export class Decoder {
}

decodeUtf8String(byteLength: number, headOffset: number): string {
if (byteLength > this.maxStrLength) {
throw new Error(
`Max length exceeded: UTF-8 byte length (${byteLength}) > maxStrLength (${this.maxStrLength})`,
);
}

const object = utf8Decode(this.view, this.pos + headOffset, byteLength);
this.pos += headOffset + byteLength;
return object;
}

decodeBinary(byteLength: number, headOffset: number): Uint8Array {
if (byteLength > this.maxBinLength) {
throw new Error(`Max length exceeded: bin length (${byteLength}) > maxBinLength (${this.maxBinLength})`);
}

if (!this.hasRemaining(byteLength + headOffset)) {
throw MORE_DATA;
}
Expand All @@ -390,6 +421,10 @@ export class Decoder {
}

decodeExtension(size: number, headOffset: number): unknown {
if (size > this.maxExtLength) {
throw new Error(`Max length exceeded: ext length (${size}) > maxExtLength (${this.maxExtLength})`);
}

const extType = this.view.getInt8(this.pos + headOffset);
const data = this.decodeBinary(size, headOffset + 1 /* extType */);
return this.extensionCodec.decode(data, extType);
Expand Down
39 changes: 37 additions & 2 deletions src/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,46 @@ import { Decoder } from "./Decoder";
export type DecodeOptions = Partial<
Readonly<{
extensionCodec: ExtensionCodecType;

/**
* Maximum string length.
* Default to 4_294_967_295 (UINT32_MAX).
*/
maxStrLength: number;
/**
* Maximum binary length.
* Default to 4_294_967_295 (UINT32_MAX).
*/
maxBinLength: number;
/**
* Maximum array length.
* Default to 4_294_967_295 (UINT32_MAX).
*/
maxArrayLength: number;
/**
* Maximum map length.
* Default to 4_294_967_295 (UINT32_MAX).
*/
maxMapLength: number;
/**
* Maximum extension length.
* Default to 4_294_967_295 (UINT32_MAX).
*/
maxExtLength: number;
}>
>;

export function decode(buffer: ReadonlyArray<number> | Uint8Array, options?: DecodeOptions): unknown {
const decoder = new Decoder(options && options.extensionCodec);
export const defaultDecodeOptions: DecodeOptions = {};

export function decode(buffer: ReadonlyArray<number> | Uint8Array, options: DecodeOptions = defaultDecodeOptions): unknown {
const decoder = new Decoder(
options.extensionCodec,
options.maxStrLength,
options.maxBinLength,
options.maxArrayLength,
options.maxMapLength,
options.maxExtLength,
);
decoder.setBuffer(buffer); // decodeSync() requires only one buffer
return decoder.decodeOneSync();
}
19 changes: 12 additions & 7 deletions src/decodeAsync.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { ExtensionCodecType } from "./ExtensionCodec";
import { Decoder } from "./Decoder";
import { defaultDecodeOptions, DecodeOptions } from "./decode";

type DecodeAsyncOptions = Partial<
Readonly<{
extensionCodec: ExtensionCodecType;
}>
>;
export type DecodeAsyncOptions = DecodeOptions;
export const defaultDecodeAsyncOptions = defaultDecodeOptions;

export async function decodeAsync(
stream: AsyncIterable<Uint8Array | ArrayLike<number>>,
options?: DecodeAsyncOptions,
options: DecodeAsyncOptions = defaultDecodeOptions,
): Promise<unknown> {
const decoder = new Decoder(options && options.extensionCodec);
const decoder = new Decoder(
options.extensionCodec,
options.maxStrLength,
options.maxBinLength,
options.maxArrayLength,
options.maxMapLength,
options.maxExtLength,
);
return decoder.decodeOneAsync(stream);
}
95 changes: 95 additions & 0 deletions test/decode-max-length.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import assert from "assert";
import { encode, decode, decodeAsync } from "../src";
import { DecodeOptions } from "../src/decode";

describe("decode with max${Type}Length specified", () => {
async function* createStream<T>(input: T) {
yield input;
}

context("maxStrLength", () => {
const input = encode("foo");
const options: DecodeOptions = { maxStrLength: 1 };

it("throws errors (synchronous)", () => {
assert.throws(() => {
decode(input, options);
}, /max length exceeded/i);
});

it("throws errors (asynchronous)", async () => {
await assert.rejects(async () => {
await decodeAsync(createStream(input), options);
}, /max length exceeded/i);
});
});

context("maxBinLength", () => {
const input = encode(Uint8Array.from([1, 2, 3]));
const options: DecodeOptions = { maxBinLength: 1 };

it("throws errors (synchronous)", () => {
assert.throws(() => {
decode(input, options);
}, /max length exceeded/i);
});

it("throws errors (asynchronous)", async () => {
await assert.rejects(async () => {
await decodeAsync(createStream(input), options);
}, /max length exceeded/i);
});
});

context("maxArrayLength", () => {
const input = encode([1, 2, 3]);
const options: DecodeOptions = { maxArrayLength: 1 };

it("throws errors (synchronous)", () => {
assert.throws(() => {
decode(input, options);
}, /max length exceeded/i);
});

it("throws errors (asynchronous)", async () => {
await assert.rejects(async () => {
await decodeAsync(createStream(input), options);
}, /max length exceeded/i);
});
});

context("maxMapLength", () => {
const input = encode({ foo: 1, bar: 1, baz: 3 });
const options: DecodeOptions = { maxMapLength: 1 };

it("throws errors (synchronous)", () => {
assert.throws(() => {
decode(input, options);
}, /max length exceeded/i);
});

it("throws errors (asynchronous)", async () => {
await assert.rejects(async () => {
await decodeAsync(createStream(input), options);
}, /max length exceeded/i);
});
});

context("maxExtType", () => {
const input = encode(new Date());
// timextamp ext requires at least 4 bytes.
const options: DecodeOptions = { maxExtLength: 1 };

it("throws errors (synchronous)", () => {
assert.throws(() => {
decode(input, options);
}, /max length exceeded/i);
});

it("throws errors (asynchronous)", async () => {
await assert.rejects(async () => {
await decodeAsync(createStream(input), options);
}, /max length exceeded/i);
});
});
});