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

Filter by extension

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: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ deepStrictEqual(decode(encoded), object);
- [`EncoderOptions`](#encoderoptions)
- [`decode(buffer: ArrayLike<number> | BufferSource, options?: DecoderOptions): unknown`](#decodebuffer-arraylikenumber--buffersource-options-decoderoptions-unknown)
- [`DecoderOptions`](#decoderoptions)
- [`decodeSingle(buffer: ArrayLike<number> | BufferSource, options?: DecoderOptions): DecodeSingleResult`](#decodesinglebuffer-arraylikenumber--buffersource-options-decoderoptions-decodesingleresult)
- [`decodeMulti(buffer: ArrayLike<number> | BufferSource, options?: DecoderOptions): Generator<unknown, void, unknown>`](#decodemultibuffer-arraylikenumber--buffersource-options-decoderoptions-generatorunknown-void-unknown)
- [`decodeAsync(stream: ReadableStreamLike<ArrayLike<number> | BufferSource>, options?: DecoderOptions): Promise<unknown>`](#decodeasyncstream-readablestreamlikearraylikenumber--buffersource-options-decoderoptions-promiseunknown)
- [`decodeArrayStream(stream: ReadableStreamLike<ArrayLike<number> | BufferSource>, options?: DecoderOptions): AsyncIterable<unknown>`](#decodearraystreamstream-readablestreamlikearraylikenumber--buffersource-options-decoderoptions-asynciterableunknown)
Expand Down Expand Up @@ -129,7 +130,7 @@ It decodes `buffer` that includes a MessagePack-encoded object, and returns the

`buffer` must be an array of bytes, which is typically `Uint8Array` or `ArrayBuffer`. `BufferSource` is defined as `ArrayBuffer | ArrayBufferView`.

The `buffer` must include a single encoded object. If the `buffer` includes extra bytes after an object or the `buffer` is empty, it throws `RangeError`. To decode `buffer` that includes multiple encoded objects, use `decodeMulti()` or `decodeMultiStream()` (recommended) instead.
The `buffer` must include a single encoded object. If the `buffer` includes extra bytes after an object or the `buffer` is empty, it throws `RangeError`. To decode a single object from a `buffer` that may have extra bytes after it, use `decodeSingle()` instead. To decode `buffer` that includes multiple encoded objects, use `decodeMulti()` or `decodeMultiStream()` (recommended) instead.

for example:

Expand Down Expand Up @@ -164,6 +165,24 @@ To skip UTF-8 decoding of strings, `rawStrings` can be set to `true`. In this ca

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

### `decodeSingle(buffer: ArrayLike<number> | BufferSource, options?: DecoderOptions): DecodeSingleResult`

It decodes `buffer` that includes a single MessagePack-encoded object, and returns the decoded object and the number of bytes consumed. Unlike `decode()`, it does not throw `RangeError` even if the `buffer` includes extra bytes after the object.

`DecodeSingleResult` is defined as `{ value: unknown, bytesConsumed: number }`, where `value` is the decoded object and `bytesConsumed` is the position where the extra bytes begin in the `buffer`.

This is useful to handle a protocol that mixes MessagePack-encoded objects and other data, for example `[msgpack object][opaque bytes][msgpack object]`:

```typescript
import { decodeSingle } from "@msgpack/msgpack";

const buffer: Uint8Array; // includes a MessagePack-encoded object followed by other data

const { value, bytesConsumed } = decodeSingle(buffer);
console.log(value); // the first object in the buffer
const rest = buffer.subarray(bytesConsumed); // the extra bytes after the object
```

### `decodeMulti(buffer: ArrayLike<number> | BufferSource, options?: DecoderOptions): Generator<unknown, void, unknown>`

It decodes `buffer` that includes multiple MessagePack-encoded objects, and returns decoded objects as a generator. See also `decodeMultiStream()`, which is an asynchronous variant of this function.
Expand Down
43 changes: 43 additions & 0 deletions src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ export type DecoderOptions<ContextType = undefined> = Readonly<
> &
ContextOf<ContextType>;

/**
* The result of {@link Decoder#decodeSingle} and {@link decodeSingle}.
*/
export type DecodeSingleResult = {
/**
* The decoded object.
*/
value: unknown;

/**
* The number of bytes consumed to decode {@link value},
* which is the position where the extra bytes begin in the buffer.
*/
bytesConsumed: number;
};

const STATE_ARRAY = "array";
const STATE_MAP_KEY = "map_key";
const STATE_MAP_VALUE = "map_value";
Expand Down Expand Up @@ -332,6 +348,33 @@ export class Decoder<ContextType = undefined> {
}
}

/**
* It decodes a single MessagePack object in a buffer and returns the decoded object and the
* number of bytes consumed. Unlike {@link decode}, it does not throw an error even if the
* buffer has extra bytes after the object.
*
* @throws {@link RangeError} if the buffer is incomplete, including the case where the buffer is empty.
* @throws {@link DecodeError} if the buffer contains invalid data.
*/
public decodeSingle(buffer: ArrayLike<number> | ArrayBufferView | ArrayBufferLike): DecodeSingleResult {
if (this.entered) {
const instance = this.clone();
return instance.decodeSingle(buffer);
}

try {
this.entered = true;

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

const value = this.doDecodeSync();
return { value, bytesConsumed: this.pos };
} finally {
this.entered = false;
}
}

public *decodeMulti(buffer: ArrayLike<number> | ArrayBufferView | ArrayBufferLike): Generator<unknown, void, unknown> {
if (this.entered) {
const instance = this.clone();
Expand Down
19 changes: 18 additions & 1 deletion src/decode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Decoder } from "./Decoder.ts";
import type { DecoderOptions } from "./Decoder.ts";
import type { DecoderOptions, DecodeSingleResult } from "./Decoder.ts";
import type { SplitUndefined } from "./context.ts";

/**
Expand All @@ -19,6 +19,23 @@ export function decode<ContextType = undefined>(
return decoder.decode(buffer);
}

/**
* It decodes a single MessagePack object in a buffer and returns the decoded object and the
* number of bytes consumed. Unlike {@link decode}, it does not throw an error even if the
* buffer has extra bytes after the object, so it is useful to decode a buffer that contains
* non-MessagePack data after a MessagePack object.
*
* @throws {@link RangeError} if the buffer is incomplete, including the case where the buffer is empty.
* @throws {@link DecodeError} if the buffer contains invalid data.
*/
export function decodeSingle<ContextType = undefined>(
buffer: ArrayLike<number> | ArrayBufferView | ArrayBufferLike,
options?: DecoderOptions<SplitUndefined<ContextType>>,
): DecodeSingleResult {
const decoder = new Decoder(options);
return decoder.decodeSingle(buffer);
}

/**
* It decodes multiple MessagePack objects in a buffer.
* This is corresponding to {@link decodeMultiStream}.
Expand Down
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
import { encode } from "./encode.ts";
export { encode };

import { decode, decodeMulti } from "./decode.ts";
export { decode, decodeMulti };
import { decode, decodeSingle, decodeMulti } from "./decode.ts";
export { decode, decodeSingle, decodeMulti };

import { decodeAsync, decodeArrayStream, decodeMultiStream } from "./decodeAsync.ts";
export { decodeAsync, decodeArrayStream, decodeMultiStream };

import { Decoder } from "./Decoder.ts";
export { Decoder };
import type { DecoderOptions } from "./Decoder.ts";
export type { DecoderOptions };
import type { DecoderOptions, DecodeSingleResult } from "./Decoder.ts";
export type { DecoderOptions, DecodeSingleResult };
import { DecodeError } from "./DecodeError.ts";
export { DecodeError };

Expand Down
65 changes: 65 additions & 0 deletions test/decodeSingle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import assert from "assert";
import { encode, decodeSingle, Decoder } from "../src/index.ts";

describe("decodeSingle", () => {
it("decodes a single object and returns the number of bytes consumed", () => {
const item = { name: "foo" };
const encoded = encode(item);

const result = decodeSingle(encoded);

assert.deepStrictEqual(result.value, item);
assert.strictEqual(result.bytesConsumed, encoded.byteLength);
});

it("does not throw an error even if the buffer has extra bytes after the object", () => {
// simulates a framed buffer: [msgpack object][opaque bytes][msgpack object]
const firstItem = { type: "binary", size: 4 };
const secondItem = [1, 2, 3];
const opaqueBytes = Uint8Array.from([0xc1, 0xff, 0x00, 0xc1]); // 0xc1 is never used in MessagePack

const encodedFirst = encode(firstItem);
const encodedSecond = encode(secondItem);
const buffer = new Uint8Array(encodedFirst.byteLength + opaqueBytes.byteLength + encodedSecond.byteLength);
buffer.set(encodedFirst);
buffer.set(opaqueBytes, encodedFirst.byteLength);
buffer.set(encodedSecond, encodedFirst.byteLength + opaqueBytes.byteLength);

const first = decodeSingle(buffer);
assert.deepStrictEqual(first.value, firstItem);
assert.strictEqual(first.bytesConsumed, encodedFirst.byteLength);

// skip the opaque bytes, whose size is known from the first object
const second = decodeSingle(buffer.subarray(first.bytesConsumed + opaqueBytes.byteLength));
assert.deepStrictEqual(second.value, secondItem);
assert.strictEqual(second.bytesConsumed, encodedSecond.byteLength);
});

it("throws RangeError if the buffer is empty", () => {
assert.throws(() => {
decodeSingle(new Uint8Array(0));
}, RangeError);
});

it("throws RangeError if the buffer is truncated", () => {
const encoded = encode("foobar");
assert.throws(() => {
decodeSingle(encoded.subarray(0, encoded.byteLength - 1));
}, RangeError);
});

it("is also available as Decoder#decodeSingle for reusing an instance", () => {
const items = ["foo", 10];
const encoded = encode(items[0]);
const bufferWithExtraBytes = new Uint8Array(encoded.byteLength + encode(items[1]).byteLength);
bufferWithExtraBytes.set(encoded);
bufferWithExtraBytes.set(encode(items[1]), encoded.byteLength);

const decoder = new Decoder();
for (let i = 0; i < 3; i++) {
const result = decoder.decodeSingle(bufferWithExtraBytes);
assert.deepStrictEqual(result.value, items[0]);
assert.strictEqual(result.bytesConsumed, encoded.byteLength);
}
});
});