From 5295681f7e2754c32daf99d5336f5c3c94cd2463 Mon Sep 17 00:00:00 2001 From: FUJI Goro Date: Tue, 4 May 2021 11:06:16 +0900 Subject: [PATCH] reset Decoder's state to recover from broken input --- src/Decoder.ts | 5 +++- src/Encoder.ts | 2 +- test/edge-cases.test.ts | 64 +++++++++++++++++++++++++++++++++-------- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/Decoder.ts b/src/Decoder.ts index 8897aea9..4a1f4574 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -76,7 +76,7 @@ export class DecodeError extends Error { } } -export class Decoder { +export class Decoder { private totalPos = 0; private pos = 0; @@ -99,6 +99,9 @@ export class Decoder { private reinitializeState() { this.totalPos = 0; this.headByte = HEAD_BYTE_REQUIRED; + this.stack.length = 0; + + // view, bytes, and pos will be re-initialized in setBuffer() } private setBuffer(buffer: ArrayLike | BufferSource): void { diff --git a/src/Encoder.ts b/src/Encoder.ts index 38893798..02b413b8 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -7,7 +7,7 @@ import type { ExtData } from "./ExtData"; export const DEFAULT_MAX_DEPTH = 100; export const DEFAULT_INITIAL_BUFFER_SIZE = 2048; -export class Encoder { +export class Encoder { private pos = 0; private view = new DataView(new ArrayBuffer(this.initialBufferSize)); private bytes = new Uint8Array(this.view.buffer); diff --git a/test/edge-cases.test.ts b/test/edge-cases.test.ts index 9dfb04e4..8214b7a6 100644 --- a/test/edge-cases.test.ts +++ b/test/edge-cases.test.ts @@ -1,36 +1,63 @@ +// kind of hand-written fuzzing data +// any errors should not break Encoder/Decoder instance states import assert from "assert"; -import { encode, decode, decodeAsync } from "../src"; +import { encode, decode, decodeAsync, Encoder, Decoder } from "../src"; import { DataViewIndexOutOfBoundsError } from "../src/Decoder"; +function testEncoder(encoder: Encoder): void { + const object = { + foo: 1, + bar: 2, + baz: ["one", "two", "three"], + }; + assert.deepStrictEqual(decode(encoder.encode(object)), object); +} + +function testDecoder(decoder: Decoder): void { + const object = { + foo: 1, + bar: 2, + baz: ["one", "two", "three"], + }; + assert.deepStrictEqual(decoder.decode(encode(object)), object); +} + describe("edge cases", () => { context("try to encode cyclic refs", () => { it("throws errors on arrays", () => { + const encoder = new Encoder(); const cyclicRefs: Array = []; cyclicRefs.push(cyclicRefs); assert.throws(() => { - encode(cyclicRefs); + encoder.encode(cyclicRefs); }, /too deep/i); + testEncoder(encoder); }); it("throws errors on objects", () => { + const encoder = new Encoder(); const cyclicRefs: Record = {}; cyclicRefs["foo"] = cyclicRefs; assert.throws(() => { - encode(cyclicRefs); + encoder.encode(cyclicRefs); }, /too deep/i); + testEncoder(encoder); }); }); - context("try to encode non-encodable objects", () => { + context("try to encode unrecognized objects", () => { it("throws errors", () => { + const encoder = new Encoder(); assert.throws(() => { encode(() => {}); }, /unrecognized object/i); + testEncoder(encoder); }); }); context("try to decode a map with non-string keys (asynchronous)", () => { it("throws errors", async () => { + const decoder = new Decoder(); const createStream = async function* () { yield [0x81]; // fixmap size=1 yield encode(null); @@ -38,73 +65,86 @@ describe("edge cases", () => { }; await assert.rejects(async () => { - await decodeAsync(createStream()); + await decoder.decodeAsync(createStream()); }, /The type of key must be string/i); + testDecoder(decoder); }); }); - context("try to decode invlid MessagePack binary", () => { + context("try to decode invalid MessagePack binary", () => { it("throws errors", () => { + const decoder = new Decoder(); const TYPE_NEVER_USED = 0xc1; assert.throws(() => { - decode([TYPE_NEVER_USED]); + decoder.decode([TYPE_NEVER_USED]); }, /unrecognized type byte/i); + testDecoder(decoder); }); }); context("try to decode insufficient data", () => { it("throws errors (synchronous)", () => { + const decoder = new Decoder(); assert.throws(() => { - decode([ + decoder.decode([ 0x92, // fixarray size=2 0xc0, // nil ]); // [IE11] A raw error thrown by DataView }, DataViewIndexOutOfBoundsError); + testDecoder(decoder); }); it("throws errors (asynchronous)", async () => { + const decoder = new Decoder(); const createStream = async function* () { yield [0x92]; // fixarray size=2 yield encode(null); }; await assert.rejects(async () => { - await decodeAsync(createStream()); + await decoder.decodeAsync(createStream()); }, RangeError); + testDecoder(decoder); }); }); context("try to decode data with extra bytes", () => { it("throws errors (synchronous)", () => { + const decoder = new Decoder(); assert.throws(() => { - decode([ + decoder.decode([ 0x90, // fixarray size=0 ...encode(null), ]); }, RangeError); + testDecoder(decoder); }); it("throws errors (asynchronous)", async () => { + const decoder = new Decoder(); const createStream = async function* () { yield [0x90]; // fixarray size=0 yield encode(null); }; await assert.rejects(async () => { - await decodeAsync(createStream()); + await decoder.decodeAsync(createStream()); }, RangeError); + testDecoder(decoder); }); it("throws errors (asynchronous)", async () => { + const decoder = new Decoder(); const createStream = async function* () { yield [0x90, ...encode(null)]; // fixarray size=0 + nil }; await assert.rejects(async () => { - await decodeAsync(createStream()); + await decoder.decodeAsync(createStream()); }, RangeError); + testDecoder(decoder); }); }); });