std/msgpack/encode.ts
lionel-rowe 15fc72c273
fix(msgpack): accept readonly input data in encode() (#5832)
feat(msgpack): accept readonly input data (#5831)
2024-08-27 15:16:03 +10:00

286 lines
7.8 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
import { concat } from "@std/bytes/concat";
/**
* Value types that can be encoded to MessagePack.
*/
export type ValueType =
| number
| bigint
| string
| boolean
| null
| Uint8Array
| readonly ValueType[]
| ValueMap;
/**
* Value map that can be encoded to MessagePack.
*/
export interface ValueMap {
/** Value map entry */
[index: string | number]: ValueType;
}
const FOUR_BITS = 16;
const FIVE_BITS = 32;
const SEVEN_BITS = 128;
const EIGHT_BITS = 256;
const FIFTEEN_BITS = 32768;
const SIXTEEN_BITS = 65536;
const THIRTY_ONE_BITS = 2147483648;
const THIRTY_TWO_BITS = 4294967296;
const SIXTY_THREE_BITS = 9223372036854775808n;
const SIXTY_FOUR_BITS = 18446744073709551616n;
const encoder = new TextEncoder();
/**
* Encode a value to {@link https://msgpack.org/ | MessagePack} binary format.
*
* @example Usage
* ```ts
* import { encode } from "@std/msgpack/encode";
* import { assertEquals } from "@std/assert";
*
* const obj = {
* str: "deno",
* arr: [1, 2, 3],
* map: {
* foo: "bar"
* }
* }
*
* const encoded = encode(obj);
*
* assertEquals(encoded.length, 31);
* ```
*
* @param object Value to encode to MessagePack binary format.
* @returns Encoded MessagePack binary data.
*/
export function encode(object: ValueType): Uint8Array {
const byteParts: Uint8Array[] = [];
encodeSlice(object, byteParts);
return concat(byteParts);
}
function encodeFloat64(num: number) {
const dataView = new DataView(new ArrayBuffer(9));
dataView.setFloat64(1, num);
dataView.setUint8(0, 0xcb);
return new Uint8Array(dataView.buffer);
}
function encodeNumber(num: number) {
if (!Number.isInteger(num)) { // float 64
return encodeFloat64(num);
}
if (num < 0) {
if (num >= -FIVE_BITS) { // negative fixint
return new Uint8Array([num]);
}
if (num >= -SEVEN_BITS) { // int 8
return new Uint8Array([0xd0, num]);
}
if (num >= -FIFTEEN_BITS) { // int 16
const dataView = new DataView(new ArrayBuffer(3));
dataView.setInt16(1, num);
dataView.setUint8(0, 0xd1);
return new Uint8Array(dataView.buffer);
}
if (num >= -THIRTY_ONE_BITS) { // int 32
const dataView = new DataView(new ArrayBuffer(5));
dataView.setInt32(1, num);
dataView.setUint8(0, 0xd2);
return new Uint8Array(dataView.buffer);
}
// float 64
return encodeFloat64(num);
}
// if the number fits within a positive fixint, use it
if (num <= 0x7f) {
return new Uint8Array([num]);
}
if (num < EIGHT_BITS) { // uint8
return new Uint8Array([0xcc, num]);
}
if (num < SIXTEEN_BITS) { // uint16
const dataView = new DataView(new ArrayBuffer(3));
dataView.setUint16(1, num);
dataView.setUint8(0, 0xcd);
return new Uint8Array(dataView.buffer);
}
if (num < THIRTY_TWO_BITS) { // uint32
const dataView = new DataView(new ArrayBuffer(5));
dataView.setUint32(1, num);
dataView.setUint8(0, 0xce);
return new Uint8Array(dataView.buffer);
}
// float 64
return encodeFloat64(num);
}
function encodeSlice(object: ValueType, byteParts: Uint8Array[]) {
if (object === null) {
byteParts.push(new Uint8Array([0xc0]));
return;
}
if (object === false) {
byteParts.push(new Uint8Array([0xc2]));
return;
}
if (object === true) {
byteParts.push(new Uint8Array([0xc3]));
return;
}
if (typeof object === "number") {
byteParts.push(encodeNumber(object));
return;
}
if (typeof object === "bigint") {
if (object < 0) {
if (object < -SIXTY_THREE_BITS) {
throw new Error("Cannot safely encode bigint larger than 64 bits");
}
const dataView = new DataView(new ArrayBuffer(9));
dataView.setBigInt64(1, object);
dataView.setUint8(0, 0xd3);
byteParts.push(new Uint8Array(dataView.buffer));
return;
}
if (object >= SIXTY_FOUR_BITS) {
throw new Error("Cannot safely encode bigint larger than 64 bits");
}
const dataView = new DataView(new ArrayBuffer(9));
dataView.setBigUint64(1, object);
dataView.setUint8(0, 0xcf);
byteParts.push(new Uint8Array(dataView.buffer));
return;
}
if (typeof object === "string") {
const encoded = encoder.encode(object);
const len = encoded.length;
if (len < FIVE_BITS) { // fixstr
byteParts.push(new Uint8Array([0xa0 | len]));
} else if (len < EIGHT_BITS) { // str 8
byteParts.push(new Uint8Array([0xd9, len]));
} else if (len < SIXTEEN_BITS) { // str 16
const dataView = new DataView(new ArrayBuffer(3));
dataView.setUint16(1, len);
dataView.setUint8(0, 0xda);
byteParts.push(new Uint8Array(dataView.buffer));
} else if (len < THIRTY_TWO_BITS) { // str 32
const dataView = new DataView(new ArrayBuffer(5));
dataView.setUint32(1, len);
dataView.setUint8(0, 0xdb);
byteParts.push(new Uint8Array(dataView.buffer));
} else {
throw new Error(
"Cannot safely encode string with size larger than 32 bits",
);
}
byteParts.push(encoded);
return;
}
if (object instanceof Uint8Array) {
if (object.length < EIGHT_BITS) { // bin 8
byteParts.push(new Uint8Array([0xc4, object.length]));
} else if (object.length < SIXTEEN_BITS) { // bin 16
const dataView = new DataView(new ArrayBuffer(3));
dataView.setUint16(1, object.length);
dataView.setUint8(0, 0xc5);
byteParts.push(new Uint8Array(dataView.buffer));
} else if (object.length < THIRTY_TWO_BITS) { // bin 32
const dataView = new DataView(new ArrayBuffer(5));
dataView.setUint32(1, object.length);
dataView.setUint8(0, 0xc6);
byteParts.push(new Uint8Array(dataView.buffer));
} else {
throw new Error(
"Cannot safely encode Uint8Array with size larger than 32 bits",
);
}
byteParts.push(object);
return;
}
if (Array.isArray(object)) {
if (object.length < FOUR_BITS) { // fixarray
byteParts.push(new Uint8Array([0x90 | object.length]));
} else if (object.length < SIXTEEN_BITS) { // array 16
const dataView = new DataView(new ArrayBuffer(3));
dataView.setUint16(1, object.length);
dataView.setUint8(0, 0xdc);
byteParts.push(new Uint8Array(dataView.buffer));
} else if (object.length < THIRTY_TWO_BITS) { // array 32
const dataView = new DataView(new ArrayBuffer(5));
dataView.setUint32(1, object.length);
dataView.setUint8(0, 0xdd);
byteParts.push(new Uint8Array(dataView.buffer));
} else {
throw new Error(
"Cannot safely encode array with size larger than 32 bits",
);
}
for (const obj of object) {
encodeSlice(obj, byteParts);
}
return;
}
// If object is a plain object
const prototype = Object.getPrototypeOf(object);
if (prototype === null || prototype === Object.prototype) {
const numKeys = Object.keys(object).length;
if (numKeys < FOUR_BITS) { // fixarray
byteParts.push(new Uint8Array([0x80 | numKeys]));
} else if (numKeys < SIXTEEN_BITS) { // map 16
const dataView = new DataView(new ArrayBuffer(3));
dataView.setUint16(1, numKeys);
dataView.setUint8(0, 0xde);
byteParts.push(new Uint8Array(dataView.buffer));
} else if (numKeys < THIRTY_TWO_BITS) { // map 32
const dataView = new DataView(new ArrayBuffer(5));
dataView.setUint32(1, numKeys);
dataView.setUint8(0, 0xdf);
byteParts.push(new Uint8Array(dataView.buffer));
} else {
throw new Error("Cannot safely encode map with size larger than 32 bits");
}
for (const [key, value] of Object.entries(object)) {
encodeSlice(key, byteParts);
encodeSlice(value, byteParts);
}
return;
}
throw new Error("Cannot safely encode value into messagepack");
}