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 4 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
190 changes: 190 additions & 0 deletions msgpack/decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

export function decode(uint8: Uint8Array) {
const { value, consumed } = decodeSlice(uint8);

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

return value;
}

function decodeString(uint8: Uint8Array, size: number) {
return decoder.decode(uint8.subarray(0, size));
}

function decodeArray(uint8: Uint8Array, size: number) {
let total = 0;
const arr: unknown[] = [];

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

return { value: arr, consumed: total };
}

function decodeMap(uint8: Uint8Array, size: number) {
let total = 0;
const map: Record<number | string, unknown> = {};

for (let i = 0; i < size; i++) {
const { value: key, consumed: keyConsumed } = decodeSlice(uint8);
uint8 = uint8.subarray(keyConsumed);
total += keyConsumed;
const { value, consumed: valueConsumed } = decodeSlice(uint8);
uint8 = uint8.subarray(valueConsumed);
total += valueConsumed;

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 { value: map, consumed: total };
}

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): { value: unknown; consumed: number } {
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
const dataView = new DataView(uint8.buffer);
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
const type = uint8[0];

if (type <= 0x7f) { // positive fixint
return { value: type, consumed: 1 };
}

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

if (((type >> 4) ^ 0b1001) === 0) { // fixarray
const size = type & 0xF;
const { value, consumed } = decodeArray(uint8.subarray(1), size);
return { value, consumed: 1 + consumed };
}

if (((type >> 5) ^ 0b101) === 0) { // fixstr
const size = type & 0b0001111;
return { value: decodeString(uint8.subarray(1), size), consumed: 1 + size };
}

if (type >= 0xe0) { // negative fixint
return {
value: dataView.getInt8(0),
consumed: 1,
};
}

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

// 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(new Uint8Array([i])), i);
}
});

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

assertEquals(decode(new Uint8Array([0b10000000 | 2, ...encodedMap])), map);
});

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

assertEquals(
decode(new Uint8Array([0b10010000 | array.length, ...array])),
array,
);
});

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

assertEquals(
decode(new Uint8Array([0xA0 | encoded.length, ...encoded])),
str,
);
});

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

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

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

Deno.test("float 32, float 64", () => {
assertEquals(
decode(new Uint8Array([0xca, 0x43, 0xd2, 0x58, 0x52])),
420.69000244140625,
aapoalas marked this conversation as resolved.
Show resolved Hide resolved
);
assertEquals(
decode(
new Uint8Array([0xcb, 0x40, 0x7A, 0x4B, 0x0A, 0x3D, 0x70, 0xA3, 0xD7]),
),
420.689999999999997726263245568,
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
);
});

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

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

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

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

assertEquals(decode(new Uint8Array([0xd9, encoded.length, ...encoded])), str);
assertEquals(
decode(new Uint8Array([0xda, 0, encoded.length, ...encoded])),
str,
);
assertEquals(
decode(new Uint8Array([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(new Uint8Array([0xdc, 0, array.length, ...array])),
array,
);
assertEquals(
decode(new Uint8Array([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(new Uint8Array([0xde, 0, 2, ...encodedMap])), map);
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
assertEquals(decode(new Uint8Array([0xdf, 0, 0, 0, 2, ...encodedMap])), map);
});

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