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(cbor): add encoding/decoding for new Map() instance #6252

Merged
merged 4 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 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:
}

Check warning on line 251 in cbor/_common_decode.ts

View check run for this annotation

Codecov / codecov/patch

cbor/_common_decode.ts#L251

Added line #L251 was not covered by tests
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 @@
`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;
}

Check warning on line 332 in cbor/_common_decode.ts

View check run for this annotation

Codecov / codecov/patch

cbor/_common_decode.ts#L330-L332

Added lines #L330 - L332 were not covered by tests
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 @@
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));
}

Check warning on line 131 in cbor/_common_encode.ts

View check run for this annotation

Codecov / codecov/patch

cbor/_common_encode.ts#L129-L131

Added lines #L129 - L131 were not covered by tests
}
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
Loading