223 lines
6.7 KiB
TypeScript
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");
|
|
});
|
|
});
|