Files
suaveplan/packages/foundation/flow/cbor/src/decoder/decoder.test.ts
2026-05-27 23:47:53 +01:00

223 lines
6.7 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import { encode } from "../encoder/encoder.js";
import { isTagged, Tagged } from "../tags/tagged.js";
import { decode, type TagHandler } from "./decoder.js";
const fromHex = (s: string): Uint8Array => {
const clean = s.replace(/\s/g, "");
const out = new Uint8Array(clean.length / 2);
for (let i = 0; i < out.length; i++)
out[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
return out;
};
describe("decode — RFC 8949 Appendix A vectors", () => {
it.each([
["00", 0],
["01", 1],
["0a", 10],
["17", 23],
["1818", 24],
["1864", 100],
["1903e8", 1000],
["1a000f4240", 1000000],
["20", -1],
["29", -10],
["3863", -100],
["3903e7", -1000],
])("integer %s → %p", { timeout: 5000 }, (hex, expected) => {
expect(decode(fromHex(hex))).toBe(expected);
});
it("u64 max → bigint", { timeout: 5000 }, () => {
expect(decode(fromHex("1bffffffffffffffff"))).toBe(
18446744073709551615n
);
});
it("negative u64 below safe → bigint", { timeout: 5000 }, () => {
expect(decode(fromHex("3bffffffffffffffff"))).toBe(
-18446744073709551616n
);
});
it.each([
["f4", false],
["f5", true],
["f6", null],
["f7", undefined],
])("simple %s → %p", { timeout: 5000 }, (hex, expected) => {
expect(decode(fromHex(hex))).toBe(expected);
});
it("byte string", { timeout: 5000 }, () => {
const v = decode(fromHex("4401020304"));
expect(v).toBeInstanceOf(Uint8Array);
expect(Array.from(v as Uint8Array)).toEqual([1, 2, 3, 4]);
});
it("empty string", { timeout: 5000 }, () => {
expect(decode(fromHex("60"))).toBe("");
});
it("text string", { timeout: 5000 }, () => {
expect(decode(fromHex("6449455446"))).toBe("IETF");
});
it("multibyte text 'ü'", { timeout: 5000 }, () => {
expect(decode(fromHex("62c3bc"))).toBe("ü");
});
it("array [1,2,3]", { timeout: 5000 }, () => {
expect(decode(fromHex("83010203"))).toEqual([1, 2, 3]);
});
it("nested array", { timeout: 5000 }, () => {
expect(decode(fromHex("8301820203820405"))).toEqual([
1,
[2, 3],
[4, 5],
]);
});
it("map {1:2,3:4}", { timeout: 5000 }, () => {
const v = decode(fromHex("a201020304")) as Map<number, number>;
expect(v.get(1)).toBe(2);
expect(v.get(3)).toBe(4);
expect(v.size).toBe(2);
});
it("tag 0 RFC 3339 string", { timeout: 5000 }, () => {
const v = decode(
fromHex("c074323031332d30332d32315432303a30343a30305a")
);
expect(isTagged(v)).toBe(true);
expect((v as Tagged).tag).toBe(0);
expect((v as Tagged).value).toBe("2013-03-21T20:04:00Z");
});
it("tag 1 epoch", { timeout: 5000 }, () => {
const v = decode(fromHex("c11a514b67b0"));
expect((v as Tagged).tag).toBe(1);
expect((v as Tagged).value).toBe(1363896240);
});
it("float16 1.5", { timeout: 5000 }, () => {
expect(decode(fromHex("f93e00"))).toBe(1.5);
});
it("float16 +Infinity", { timeout: 5000 }, () => {
expect(decode(fromHex("f97c00"))).toBe(Number.POSITIVE_INFINITY);
});
it("float16 NaN", { timeout: 5000 }, () => {
expect(Number.isNaN(decode(fromHex("f97e00")) as number)).toBe(true);
});
it("float32 100000.0", { timeout: 5000 }, () => {
expect(decode(fromHex("fa47c35000"))).toBe(100000);
});
it("float64 1.1", { timeout: 5000 }, () => {
expect(decode(fromHex("fb3ff199999999999a"))).toBe(1.1);
});
});
describe("decode — strictness", () => {
it("rejects trailing bytes in strict mode", { timeout: 5000 }, () => {
expect(() => decode(fromHex("0001"))).toThrow(/MALFORMED/);
});
it("allows trailing bytes when configured", { timeout: 5000 }, () => {
expect(decode(fromHex("0001"), { allowTrailingBytes: true })).toBe(0);
});
it("rejects invalid UTF-8", { timeout: 5000 }, () => {
expect(() => decode(fromHex("62c328"))).toThrow(/MALFORMED/);
});
it("rejects truncated input", { timeout: 5000 }, () => {
expect(() => decode(fromHex("18"))).toThrow(/MALFORMED/);
});
it("rejects unexpected break", { timeout: 5000 }, () => {
expect(() => decode(fromHex("ff"))).toThrow(/MALFORMED/);
});
it("respects maxDepth", { timeout: 5000 }, () => {
// 65 nested arrays
let bytes = fromHex("00");
for (let i = 0; i < 65; i++) {
const wrapper = new Uint8Array(bytes.length + 1);
wrapper[0] = 0x81;
wrapper.set(bytes, 1);
bytes = wrapper;
}
expect(() => decode(bytes, { maxDepth: 64 })).toThrow(
/NESTED_TOO_DEEP/
);
});
});
describe("decode — indefinite-length", () => {
it("accepts indefinite text-string", { timeout: 5000 }, () => {
expect(decode(fromHex("7f657374726561646d696e67ff"))).toBe("streaming");
});
it("accepts indefinite byte-string", { timeout: 5000 }, () => {
const v = decode(fromHex("5f42010243030405ff")) as Uint8Array;
expect(Array.from(v)).toEqual([1, 2, 3, 4, 5]);
});
it("accepts indefinite array", { timeout: 5000 }, () => {
expect(decode(fromHex("9f018202039f0405ffff"))).toEqual([
1,
[2, 3],
[4, 5],
]);
});
it("accepts indefinite map", { timeout: 5000 }, () => {
const v = decode(fromHex("bf61610161629f0203ffff")) as Map<
string,
unknown
>;
expect(v.get("a")).toBe(1);
expect(v.get("b")).toEqual([2, 3]);
});
});
describe("decode — round-trip", () => {
it("encode-then-decode preserves arrays of mixed types", {
timeout: 5000,
}, () => {
const original: unknown[] = [
1,
"hello",
true,
null,
[1, 2],
new Uint8Array([0xde, 0xad]),
];
const out = decode(encode(original));
expect(out).toEqual(original);
});
it("Tagged round-trips", { timeout: 5000 }, () => {
const t = new Tagged(99, "custom");
const round = decode(encode(t)) as Tagged;
expect(round.tag).toBe(99);
expect(round.value).toBe("custom");
});
it("registered tag handler is called", { timeout: 5000 }, () => {
const handler: TagHandler = (v) => `wrapped:${String(v)}`;
const out = decode(
encode(new Tagged(42, "x")),
{},
new Map([[42, handler]])
);
expect(out).toBe("wrapped:x");
});
});