Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: msgpack encoding #3460

Merged
merged 27 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
96c707b
feat: msgpack encoding
lino-levan Jun 22, 2023
045c51f
Merge branch 'main' into feat-msgpack
lino-levan Jun 22, 2023
f091336
chore: add copyright headers
lino-levan Jun 22, 2023
47b05ce
chore: update the copyright header
lino-levan Jun 22, 2023
64d8a75
chore: add more tests + fix bugs
lino-levan Jun 23, 2023
1003710
chore: address aapo's concerns
lino-levan Jun 23, 2023
71d4ab7
chore: fix test + fix a lot of bugs
lino-levan Jun 23, 2023
4f2d019
Merge branch 'main' into feat-msgpack
lino-levan Jun 27, 2023
2ef6d25
chore: fix typo
lino-levan Jun 27, 2023
b06ce29
Merge branch 'main' into feat-msgpack
lino-levan Jun 27, 2023
74c7a06
fix: remove debug console.log
lino-levan Jun 27, 2023
01768f9
chore: update decode typing
lino-levan Jun 27, 2023
1b73231
chore: move to Uint8Array.of
lino-levan Jun 27, 2023
4c12849
chore: extract f64 encode logic
lino-levan Jun 27, 2023
da1bbc9
chore: make it really hard to shoot yourself in the foot, even if you…
lino-levan Jun 27, 2023
7e18ffd
Merge branch 'main' into feat-msgpack
lino-levan Jun 28, 2023
a2d7709
chore: add bigints test and make number encoding stable
lino-levan Jun 28, 2023
316a9e2
chore: add tests for encode failing large bigints
lino-levan Jun 28, 2023
3c07f01
chore: move to lookahead and consume
lino-levan Jun 28, 2023
9bd40a7
chore: clarify types in comments
lino-levan Jun 28, 2023
e859a33
chore: use bitmask for fixmap/fixarray/fixstr
lino-levan Jun 28, 2023
2e238af
Merge branch 'main' into feat-msgpack
lino-levan Jul 3, 2023
b6f0722
Merge branch 'main' into feat-msgpack
lino-levan Jul 5, 2023
32e0867
Update msgpack/decode.ts
lino-levan Jul 6, 2023
a0cadf3
chore: fmt
lino-levan Jul 6, 2023
1a0aac6
Merge branch 'main' into feat-msgpack
lino-levan Jul 7, 2023
b631493
chore: add jsdoc
lino-levan Jul 7, 2023
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
217 changes: 217 additions & 0 deletions msgpack/decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

import { EncodeType } from "./encode.ts";

export function decode(uint8: Uint8Array) {
const pointer = { consumed: 0 };
const dataView = new DataView(uint8.buffer);
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
const value = decodeSlice(uint8, dataView, pointer);

if (pointer.consumed < uint8.length) {
throw new EvalError("Messagepack decode did not consume whole array");
}

return value;
}

function decodeString(
uint8: Uint8Array,
size: number,
pointer: { consumed: number },
) {
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
pointer.consumed += size;
return decoder.decode(
uint8.subarray(pointer.consumed - size, pointer.consumed),
);
}

function decodeArray(
uint8: Uint8Array,
dataView: DataView,
size: number,
pointer: { consumed: number },
) {
const arr: EncodeType[] = [];

for (let i = 0; i < size; i++) {
const value = decodeSlice(uint8, dataView, pointer);
arr.push(value);
}

return arr;
}

function decodeMap(
uint8: Uint8Array,
dataView: DataView,
size: number,
pointer: { consumed: number },
) {
const map: Record<number | string, EncodeType> = {};

for (let i = 0; i < size; i++) {
const key = decodeSlice(uint8, dataView, pointer);
const value = decodeSlice(uint8, dataView, pointer);

if (typeof key !== "number" && typeof key !== "string") {
throw new EvalError(
"Messagepack decode came across an invalid type for a key of a map",
);
}

map[key] = value;
}

return map;
}

const decoder = new TextDecoder();

/**
* Given a uint8array which contains a msgpack object,
* return the value of the object as well as how many bytes
* were consumed in obtaining this object
*/
function decodeSlice(
uint8: Uint8Array,
dataView: DataView,
pointer: { consumed: number },
): EncodeType {
const type = dataView.getUint8(pointer.consumed);
pointer.consumed++;

if (type <= 0x7f) { // positive fixint
return type;
}

if (((type >> 4) ^ 0b1000) === 0) { // fixmap
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
const size = type & 0xF;
return decodeMap(uint8, dataView, size, pointer);
}

if (((type >> 4) ^ 0b1001) === 0) { // fixarray
const size = type & 0xF;
return decodeArray(uint8, dataView, size, pointer);
}

if (((type >> 5) ^ 0b101) === 0) { // fixstr
const size = type & 0b00011111;
return decodeString(uint8, size, pointer);
}

if (type >= 0xe0) { // negative fixint
return type - 256;
}

switch (type) {
case 0xc0: // nil
return null;
case 0xc1: // (never used)
throw new Error(
"Messagepack decode encountered a type that is never used",
);
case 0xc2: // false
return false;
case 0xc3: // true
return true;
case 0xc4: { // bin 8
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
const length = dataView.getUint8(pointer.consumed);
pointer.consumed++;
const u8 = uint8.subarray(pointer.consumed, pointer.consumed + length);
pointer.consumed += length;
return u8;
}
case 0xc5: { // bin 16
const length = dataView.getUint16(pointer.consumed);
pointer.consumed += 2;
const u8 = uint8.subarray(pointer.consumed, pointer.consumed + length);
pointer.consumed += length;
return u8;
}
case 0xc6: { // bin 32
const length = dataView.getUint32(pointer.consumed);
pointer.consumed += 4;
const u8 = uint8.subarray(pointer.consumed, pointer.consumed + length);
pointer.consumed += length;
return u8;
}
case 0xc7: // ext 8
case 0xc8: // ext 16
case 0xc9: // ext 32
throw new Error("ext not implemented yet");
case 0xca: // float 32
pointer.consumed += 4;
return dataView.getFloat32(pointer.consumed - 4);
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
case 0xcb: // float 64
pointer.consumed += 8;
return dataView.getFloat64(pointer.consumed - 8);
case 0xcc: // uint 8
pointer.consumed += 1;
return dataView.getUint8(pointer.consumed - 1);
case 0xcd: // uint 16
pointer.consumed += 2;
return dataView.getUint16(pointer.consumed - 2);
case 0xce: // uint 32
pointer.consumed += 4;
return dataView.getUint32(pointer.consumed - 4);
case 0xcf: // uint 64
pointer.consumed += 8;
return dataView.getBigUint64(pointer.consumed - 8);
case 0xd0: // int 8
pointer.consumed += 1;
return dataView.getInt8(pointer.consumed - 1);
case 0xd1: // int 16
pointer.consumed += 2;
return dataView.getInt16(pointer.consumed - 2);
case 0xd2: // int 32
pointer.consumed += 4;
return dataView.getInt32(pointer.consumed - 4);
case 0xd3: // int 64
pointer.consumed += 8;
return dataView.getBigInt64(pointer.consumed - 8);
case 0xd4: // fixext 1
case 0xd5: // fixext 2
case 0xd6: // fixext 4
case 0xd7: // fixext 8
case 0xd8: // fixext 16
throw new Error("fixext not implemented yet");
case 0xd9: { // str 8
const length = dataView.getUint8(pointer.consumed);
pointer.consumed += 1;
return decodeString(uint8, length, pointer);
}
case 0xda: { // str 16
const length = dataView.getUint16(pointer.consumed);
pointer.consumed += 2;
return decodeString(uint8, length, pointer);
}
case 0xdb: { // str 32
const length = dataView.getUint32(pointer.consumed);
pointer.consumed += 4;
return decodeString(uint8, length, pointer);
}
case 0xdc: { // array 16
const length = dataView.getUint16(pointer.consumed);
pointer.consumed += 2;
return decodeArray(uint8, dataView, length, pointer);
}
case 0xdd: { // array 32
const length = dataView.getUint32(pointer.consumed);
pointer.consumed += 4;
return decodeArray(uint8, dataView, length, pointer);
}
case 0xde: { // map 16
const length = dataView.getUint16(pointer.consumed);
pointer.consumed += 2;
return decodeMap(uint8, dataView, length, pointer);
}
case 0xdf: { // map 32
const length = dataView.getUint32(pointer.consumed);
pointer.consumed += 4;
return decodeMap(uint8, dataView, length, pointer);
}
}

// All cases are covered for numbers between 0-255. Typescript isn't smart enough to know that.
throw new Error("Unreachable");
}
152 changes: 152 additions & 0 deletions msgpack/decode_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

import { assertEquals, assertThrows } from "../testing/asserts.ts";
import { decode } from "./decode.ts";

Deno.test("positive fixint", () => {
for (let i = 0; i <= 0x7f; i++) {
assertEquals(decode(Uint8Array.of(i)), i);
}
});

Deno.test("fixmap", () => {
const map = { "a": 2, "b": 3 };
const encodedMap = [0b1010_0001, 97, 2, 0b1010_0001, 98, 3];

assertEquals(decode(Uint8Array.of(0b10000000 | 2, ...encodedMap)), map);
});

Deno.test("fixarray", () => {
const array = [0, 1, 2, 3, 4, 5, 6];

assertEquals(
decode(Uint8Array.of(0b10010000 | array.length, ...array)),
array,
);
});

Deno.test("fixstr", () => {
const str = "hello world!";
const encoded = new TextEncoder().encode(str);

assertEquals(
decode(Uint8Array.of(0xA0 | encoded.length, ...encoded)),
str,
);
});

Deno.test("nil, (never used), false, true", () => {
assertEquals(decode(Uint8Array.of(0xc0)), null); // nil
assertThrows(() => decode(Uint8Array.of(0xc1))); // (never used)
assertEquals(decode(Uint8Array.of(0xc2)), false); // false
assertEquals(decode(Uint8Array.of(0xc3)), true); // true
});

Deno.test("bin 8, bin 16, bin 32", () => {
const arr = Uint8Array.of(0, 1, 2, 3, 4, 5, 6, 7);
assertEquals(decode(Uint8Array.of(0xc4, arr.length, ...arr)), arr);
assertEquals(decode(Uint8Array.of(0xc5, 0, arr.length, ...arr)), arr);
assertEquals(
decode(Uint8Array.of(0xc6, 0, 0, 0, arr.length, ...arr)),
arr,
);
});

Deno.test("ext 8, ext 16, ext 32", () => {
assertThrows(() => decode(Uint8Array.of(0xc7)));
assertThrows(() => decode(Uint8Array.of(0xc8)));
assertThrows(() => decode(Uint8Array.of(0xc9)));
});

Deno.test("float 32, float 64", () => {
assertEquals(
decode(Uint8Array.of(0xca, 0x43, 0xd2, 0x58, 0x52)),
420.69000244140625,
aapoalas marked this conversation as resolved.
Show resolved Hide resolved
);
assertEquals(
decode(
Uint8Array.of(0xcb, 0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18),
),
3.14159265358979311599796346854,
);
});

Deno.test("uint8, uint16, uint32, uint64", () => {
assertEquals(decode(Uint8Array.of(0xcc, 0xff)), 255);
assertEquals(decode(Uint8Array.of(0xcd, 0xff, 0xff)), 65535);
assertEquals(
decode(Uint8Array.of(0xce, 0xff, 0xff, 0xff, 0xff)),
4294967295,
);
assertEquals(
decode(
Uint8Array.of(0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff),
),
18446744073709551615n,
);
});

Deno.test("int8, int16, int32, int64", () => {
assertEquals(decode(Uint8Array.of(0xd0, 0x80)), -128);
assertEquals(decode(Uint8Array.of(0xd1, 0x80, 0x00)), -32768);
assertEquals(
decode(Uint8Array.of(0xd2, 0x80, 0x00, 0x00, 0x00)),
-2147483648,
);
assertEquals(
decode(
Uint8Array.of(0xd3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
),
-9223372036854775808n,
);
});

Deno.test("fixext 1, fixext 2, fixext 4, fixext 8, fixext 16", () => {
assertThrows(() => decode(Uint8Array.of(0xd4)));
assertThrows(() => decode(Uint8Array.of(0xd5)));
assertThrows(() => decode(Uint8Array.of(0xd6)));
assertThrows(() => decode(Uint8Array.of(0xd7)));
assertThrows(() => decode(Uint8Array.of(0xd8)));
});

Deno.test("str 8, str 16, str 32", () => {
const str = "hello world!";
const encoded = new TextEncoder().encode(str);

assertEquals(decode(Uint8Array.of(0xd9, encoded.length, ...encoded)), str);
assertEquals(
decode(Uint8Array.of(0xda, 0, encoded.length, ...encoded)),
str,
);
assertEquals(
decode(Uint8Array.of(0xdb, 0, 0, 0, encoded.length, ...encoded)),
str,
);
});

Deno.test("array 16, array 32", () => {
const array = [0, 1, 2, 3, 4, 5, 6];

assertEquals(
decode(Uint8Array.of(0xdc, 0, array.length, ...array)),
array,
);
assertEquals(
decode(Uint8Array.of(0xdd, 0, 0, 0, array.length, ...array)),
array,
);
});

Deno.test("map 16, map 32", () => {
const map = { "a": 2, "b": 3 };
const encodedMap = [0b1010_0001, 97, 2, 0b1010_0001, 98, 3];

assertEquals(decode(Uint8Array.of(0xde, 0, 2, ...encodedMap)), map);
assertEquals(decode(Uint8Array.of(0xdf, 0, 0, 0, 2, ...encodedMap)), map);
});

Deno.test("negative fixint", () => {
for (let i = -32; i <= -1; i++) {
assertEquals(decode(Uint8Array.of(i)), i);
}
});
Loading