Skip to content

Commit

Permalink
feat(cbor): add encoding/decoding for new Map() instance (#6252)
Browse files Browse the repository at this point in the history
  • Loading branch information
BlackAsLight authored Dec 12, 2024
1 parent 7416c34 commit 283cf34
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 12 deletions.
59 changes: 54 additions & 5 deletions cbor/_common_decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,29 +234,34 @@ export function decodeFive(
export function decodeSix(
source: number[],
aI: number,
): Date | CborTag<CborType> {
): Date | Map<CborType, CborType> | CborTag<CborType> {
if (aI > 27) {
throw new RangeError(
`Cannot decode value (0b110_${aI.toString(2).padStart(5, "0")})`,
);
}
const tagNumber = decodeZero(source, aI);
const tagContent = decode(source);
switch (BigInt(tagNumber)) {
case 0n:
case 0n: {
const tagContent = decode(source);
if (typeof tagContent !== "string") {
throw new TypeError('Invalid TagItem: Expected a "text string"');
}
return new Date(tagContent);
case 1n:
}
case 1n: {
const tagContent = decode(source);
if (typeof tagContent !== "number" && typeof tagContent !== "bigint") {
throw new TypeError(
'Invalid TagItem: Expected a "integer" or "float"',
);
}
return new Date(Number(tagContent) * 1000);
}
case 259n:
return decodeMap(source);
}
return new CborTag(tagNumber, tagContent);
return new CborTag(tagNumber, decode(source));
}

export function decodeSeven(
Expand Down Expand Up @@ -285,3 +290,47 @@ export function decodeSeven(
`Cannot decode value (0b111_${aI.toString(2).padStart(5, "0")})`,
);
}

function decodeMap(source: number[]): Map<CborType, CborType> {
const byte = source.pop();
if (byte == undefined) throw new RangeError("More bytes were expected");

const majorType = byte >> 5;
if (majorType !== 5) throw new TypeError('Invalid TagItem: Expected a "map"');
const aI = byte & 0b000_11111;
if (aI <= 27) {
const map = new Map<CborType, CborType>();
// Can safely assume `source.length < 2 ** 53` as JavaScript doesn't support an `Array` being that large.
// 2 ** 53 is the tipping point where integers loose precision.
const len = Number(decodeZero(source, aI));
for (let i = 0; i < len; ++i) {
const key = decode(source);
if (map.has(key)) {
throw new TypeError(
`A Map cannot have duplicate keys: Key (${key}) already exists`,
); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps
}
map.set(key, decode(source));
}
return map;
}
if (aI === 31) {
const map = new Map<CborType, CborType>();
if (!source.length) throw new RangeError("More bytes were expected");
while (source[source.length - 1] !== 0b111_11111) {
const key = decode(source);
if (map.has(key)) {
throw new TypeError(
`A Map cannot have duplicate keys: Key (${key}) already exists`,
); // https://datatracker.ietf.org/doc/html/rfc8949#name-specifying-keys-for-maps
}
map.set(key, decode(source));
if (!source.length) throw new RangeError("More bytes were expected");
}
source.pop();
return map;
}
throw new RangeError(
`Cannot decode value (0b101_${aI.toString(2).padStart(5, "0")})`,
);
}
31 changes: 31 additions & 0 deletions cbor/_common_encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,37 @@ export function encodeDate(x: Date): Uint8Array {
return output;
}

export function encodeMap(x: Map<CborType, CborType>): Uint8Array {
const len = x.size;
let head: Uint8Array;
if (len < 24) head = Uint8Array.from([0b101_00000 + len]);
else if (len < 2 ** 8) head = Uint8Array.from([0b101_11000, len]);
else {
head = new Uint8Array(9);
const view = new DataView(head.buffer);
if (len < 2 ** 16) {
head[0] = 0b101_11001;
view.setUint16(1, len);
head = head.subarray(0, 3);
} else if (len < 2 ** 32) {
head[0] = 0b101_11010;
view.setUint32(1, len);
head = head.subarray(0, 5);
} else {
head[0] = 0b101_11011;
view.setBigUint64(1, BigInt(len));
}
}
return concat([
Uint8Array.from([217, 1, 3]), // TagNumber 259
head,
...Array.from(x
.entries())
.map(([k, v]) => [encodeCbor(k), encodeCbor(v)])
.flat(),
]);
}

export function encodeArray(x: CborType[]): Uint8Array {
let head: number[];
if (x.length < 24) head = [0b100_00000 + x.length];
Expand Down
4 changes: 3 additions & 1 deletion cbor/decode_cbor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { CborType } from "./types.ts";
* @example Usage
* ```ts
* import { assert, assertEquals } from "@std/assert";
* import { decodeCbor, encodeCbor } from "@std/cbor";
* import { type CborType, decodeCbor, encodeCbor } from "@std/cbor";
*
* const rawMessage = [
* "Hello World",
Expand All @@ -31,6 +31,8 @@ import type { CborType } from "./types.ts";
* -1,
* null,
* Uint8Array.from([0, 1, 2, 3]),
* new Date(),
* new Map<CborType, CborType>([[1, 2], ['3', 4], [[5], { a: 6 }]]),
* ];
*
* const encodedMessage = encodeCbor(rawMessage);
Expand Down
4 changes: 3 additions & 1 deletion cbor/decode_cbor_sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { CborType } from "./types.ts";
* @example Usage
* ```ts
* import { assertEquals } from "@std/assert";
* import { decodeCborSequence, encodeCborSequence } from "@std/cbor";
* import { type CborType, decodeCborSequence, encodeCborSequence } from "@std/cbor";
*
* const rawMessage = [
* "Hello World",
Expand All @@ -30,6 +30,8 @@ import type { CborType } from "./types.ts";
* -1,
* null,
* Uint8Array.from([0, 1, 2, 3]),
* new Date(),
* new Map<CborType, CborType>([[1, 2], ['3', 4], [[5], { a: 6 }]]),
* ];
*
* const encodedMessage = encodeCborSequence(rawMessage);
Expand Down
161 changes: 161 additions & 0 deletions cbor/decode_cbor_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { random } from "./_common_test.ts";
import { decodeCbor } from "./decode_cbor.ts";
import { encodeCbor } from "./encode_cbor.ts";
import { CborTag } from "./tag.ts";
import type { CborType } from "./types.ts";

Deno.test("decodeCbor() decoding undefined", () => {
assertEquals(decodeCbor(encodeCbor(undefined)), undefined);
Expand Down Expand Up @@ -111,6 +112,47 @@ Deno.test("decodeCbor() decoding Dates", () => {
assertEquals(decodeCbor(encodeCbor(date)), date);
});

Deno.test("decodeCbor() decoding Map<CborType, CborType>", () => {
const map = new Map<CborType, CborType>([[1, 2], ["3", 4], [[5], { a: 6 }]]);
assertEquals(decodeCbor(encodeCbor(map)), map);
});

Deno.test("decodeCbor() decoding Maps", () => {
let pairs = random(0, 24);
let map = new Map(
new Array(pairs)
.fill(0)
.map((_, i) => [i, i]),
);
assertEquals(decodeCbor(encodeCbor(map)), map);

pairs = random(24, 2 ** 8);
map = new Map(
new Array(pairs)
.fill(0)
.map((_, i) => [i, i]),
);
assertEquals(decodeCbor(encodeCbor(map)), map);

pairs = random(2 ** 8, 2 ** 16);
map = new Map(
new Array(pairs)
.fill(0)
.map((_, i) => [i, i]),
);
assertEquals(decodeCbor(encodeCbor(map)), map);

pairs = random(2 ** 16, 2 ** 17);
map = new Map(
new Array(pairs)
.fill(0)
.map((_, i) => [i, i]),
);
assertEquals(decodeCbor(encodeCbor(map)), map);

// Can't test the next bracket up due to JavaScript limitations.
});

Deno.test("decodeCbor() decoding arrays", () => {
let array = new Array(random(0, 24)).fill(0).map((_) => random(0, 2 ** 32));
assertEquals(decodeCbor(encodeCbor(array)), array);
Expand Down Expand Up @@ -653,3 +695,122 @@ Deno.test("decodeCbor() rejecting majorType 7 due to additional information", ()
"Cannot decode value (0b111_11110)",
);
});

Deno.test("decodeCbor() rejecting tagNumber 259 due to additional information", () => {
assertThrows(
() => {
decodeCbor(
Uint8Array.from([
217,
1,
3,
0b101_11100,
...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)),
]),
);
},
RangeError,
"Cannot decode value (0b101_11100)",
);
assertThrows(
() => {
decodeCbor(
Uint8Array.from([
217,
1,
3,
0b101_11101,
...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)),
]),
);
},
RangeError,
"Cannot decode value (0b101_11101)",
);
assertThrows(
() => {
decodeCbor(
Uint8Array.from([
217,
1,
3,
0b101_11110,
...new Array(random(0, 64)).fill(0).map((_) => random(0, 256)),
]),
);
},
RangeError,
"Cannot decode value (0b101_11110)",
);
});

Deno.test("decodeCbor() rejecting TagNumber 259 due to maps having invalid keys", () => {
assertThrows(
() => {
decodeCbor(
Uint8Array.from([
217,
1,
3,
0b101_00010,
0b011_00001,
48,
0b000_00000,
0b011_00001,
48,
0b000_00001,
]),
);
},
TypeError,
"A Map cannot have duplicate keys: Key (0) already exists",
);
});

Deno.test("decodeCbor() rejecting tagNumber 259 due to invalid indefinite length maps", () => {
assertThrows(
() => {
decodeCbor(Uint8Array.from([217, 1, 3, 0b101_11111]));
},
RangeError,
"More bytes were expected",
);
assertThrows(
() => {
decodeCbor(
Uint8Array.from([
217,
1,
3,
0b101_11111,
0b011_00001,
48,
0b000_00000,
0b011_00001,
48,
0b000_00001,
0b111_11111,
]),
);
},
TypeError,
"A Map cannot have duplicate keys: Key (0) already exists",
);
assertThrows(
() => {
decodeCbor(
Uint8Array.from([
217,
1,
3,
0b101_11111,
0b011_00001,
48,
0b000_00000,
]),
);
},
RangeError,
"More bytes were expected",
);
});
6 changes: 5 additions & 1 deletion cbor/encode_cbor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
encodeArray,
encodeBigInt,
encodeDate,
encodeMap,
encodeNumber,
encodeObject,
encodeString,
Expand All @@ -21,7 +22,7 @@ import type { CborType } from "./types.ts";
* @example Usage
* ```ts
* import { assert, assertEquals } from "@std/assert";
* import { decodeCbor, encodeCbor } from "@std/cbor";
* import { type CborType, decodeCbor, encodeCbor } from "@std/cbor";
*
* const rawMessage = [
* "Hello World",
Expand All @@ -31,6 +32,8 @@ import type { CborType } from "./types.ts";
* -1,
* null,
* Uint8Array.from([0, 1, 2, 3]),
* new Date(),
* new Map<CborType, CborType>([[1, 2], ['3', 4], [[5], { a: 6 }]]),
* ];
*
* const encodedMessage = encodeCbor(rawMessage);
Expand Down Expand Up @@ -61,5 +64,6 @@ export function encodeCbor(value: CborType): Uint8Array {
if (value instanceof Uint8Array) return encodeUint8Array(value);
if (value instanceof Array) return encodeArray(value);
if (value instanceof CborTag) return encodeTag(value);
if (value instanceof Map) return encodeMap(value);
return encodeObject(value);
}
4 changes: 3 additions & 1 deletion cbor/encode_cbor_sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { CborType } from "./types.ts";
* @example Usage
* ```ts
* import { assertEquals } from "@std/assert";
* import { decodeCborSequence, encodeCborSequence } from "@std/cbor";
* import { type CborType, decodeCborSequence, encodeCborSequence } from "@std/cbor";
*
* const rawMessage = [
* "Hello World",
Expand All @@ -22,6 +22,8 @@ import type { CborType } from "./types.ts";
* -1,
* null,
* Uint8Array.from([0, 1, 2, 3]),
* new Date(),
* new Map<CborType, CborType>([[1, 2], ['3', 4], [[5], { a: 6 }]]),
* ];
*
* const encodedMessage = encodeCborSequence(rawMessage);
Expand Down
Loading

0 comments on commit 283cf34

Please sign in to comment.