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; 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"); }); });