import { describe, expect, it } from "vitest"; import fs from "node:fs"; import path from "node:path"; import { parseMct, parseImport, parseDump, parseKeyBin } from "@/lib/rfid/parsers"; import { exportTag } from "@/lib/rfid/exporters"; import { mifareClassicDumpSchema } from "@/lib/validation/rfid"; import { isMifareClassicDump } from "@/lib/rfid/dump-types"; const sampleMct = `+UID: 04A1B2C3 +Sector: 0 04A1B2C304A1B2C304A1B2C304A1B2C3 00000000000000000000000000000000 00000000000000000000000000000000 FFFFFFFFFFFFFF078069FFFFFFFFFFFF +Sector: 1 00000000000000000000000000000000 00000000000000000000000000000000 00000000000000000000000000000000 FFFFFFFFFFFFFF078069FFFFFFFFFFFF `; describe("MCT parser/exporter", () => { it("parses MCT dumps", () => { const tag = parseMct(sampleMct); expect(tag.uid).toBe("04A1B2C3"); expect(tag.protocol).toBe("MIFARE_CLASSIC_1K"); expect(mifareClassicDumpSchema.safeParse(tag.dumpData).success).toBe(true); }); it("round-trips via export mct", () => { const tag = parseMct(sampleMct); const exported = exportTag( { id: "00000000-0000-4000-8000-000000000001", label: "test", frequency: tag.frequency, protocol: tag.protocol, uid: tag.uid, dumpData: tag.dumpData, keys: null, notes: null, }, "mct" ); const again = parseImport(exported.body, "test.mct"); expect(again.uid).toBe(tag.uid); expect(again.protocol).toBe(tag.protocol); }); it("parses canonical JSON", () => { const json = JSON.stringify({ label: "Dock", frequency: "HF", protocol: "MIFARE_CLASSIC_1K", uid: "04:A1:B2:C3", dumpData: { size: "1K", sectors: [{ index: 0, blocks: ["AABBCCDDEEFF00112233445566778899"] }], }, }); const tag = parseImport(json, "x.json"); expect(tag.uid).toBe("04A1B2C3"); expect(tag.label).toBe("Dock"); }); }); describe("Proxmark dump import", () => { const fixturePath = path.join( import.meta.dirname, "../fixtures/hf-mf-91A2F020-dump.json" ); const proxmarkV2 = fs.readFileSync(fixturePath, "utf8"); it("parses mfc v2 JSON with object blocks and SectorKeys", () => { const tag = parseImport(proxmarkV2, "hf-mf-91A2F020-dump.json"); expect(tag.uid).toBe("91A2F020"); expect(tag.protocol).toBe("MIFARE_CLASSIC_1K"); expect(mifareClassicDumpSchema.safeParse(tag.dumpData).success).toBe(true); expect(isMifareClassicDump(tag.dumpData)).toBe(true); if (isMifareClassicDump(tag.dumpData)) { expect(tag.dumpData.sectors).toHaveLength(16); expect(tag.dumpData.sectors[0].blocks[0]).toBe( "91A2F020E30804000497F47A359E4798" ); } expect(tag.keys?.A?.[0]).toBe("A1D4FAFD4CAF"); expect(tag.keys?.B?.[0]).toBe("FFFFFFFFFFFF"); expect(tag.keys?.A?.[1]).toBe("2A2C13CC242A"); expect(tag.keys?.A).toHaveLength(16); }); it("parses mfc v2 JSON pasted inside a markdown fence", () => { const tag = parseImport( "```json\n" + proxmarkV2 + "\n```", "paste.txt" ); expect(tag.uid).toBe("91A2F020"); expect(tag.keys?.A?.[0]).toBe("A1D4FAFD4CAF"); }); it("parses a 192-byte 1K key.bin", () => { const bytes = new Uint8Array(192); bytes.set(hexToBytes("A1D4FAFD4CAF"), 0); bytes.set(hexToBytes("FFFFFFFFFFFF"), 6); bytes.set(hexToBytes("2A2C13CC242A"), 12); bytes.set(hexToBytes("FFFFFFFFFFFF"), 18); const keys = parseKeyBin(bytes, "hf-mf-91A2F020-key.bin"); expect(keys.A).toHaveLength(16); expect(keys.A?.[0]).toBe("A1D4FAFD4CAF"); expect(keys.B?.[0]).toBe("FFFFFFFFFFFF"); expect(keys.A?.[1]).toBe("2A2C13CC242A"); }); it("merges key.bin onto a dump that has no SectorKeys", () => { const dump = JSON.parse(proxmarkV2) as { SectorKeys?: unknown }; delete dump.SectorKeys; const keys = new Uint8Array(192); keys.set(hexToBytes("AABBCCDDEEFF"), 0); keys.set(hexToBytes("112233445566"), 6); const tag = parseDump({ text: JSON.stringify(dump), filename: "hf-mf-91A2F020-dump.json", keysBytes: keys, keysFilename: "hf-mf-91A2F020-key.bin", }); expect(tag.keys?.A?.[0]).toBe("AABBCCDDEEFF"); expect(tag.keys?.B?.[0]).toBe("112233445566"); }); it("parses a 1024-byte dump.bin", () => { const bytes = new Uint8Array(1024); const block0 = hexToBytes("91A2F020E30804000497F47A359E4798"); bytes.set(block0, 0); const tag = parseDump({ bytes, filename: "hf-mf-91A2F020.bin" }); expect(tag.uid).toBe("91A2F020"); expect(tag.protocol).toBe("MIFARE_CLASSIC_1K"); }); it("rejects key.bin uploaded as the dump", () => { const bytes = new Uint8Array(192); expect(() => parseDump({ bytes, filename: "hf-mf-91A2F020-key.bin" }) ).toThrow(/key\.bin/i); }); }); function hexToBytes(hex: string): Uint8Array { const out = new Uint8Array(hex.length / 2); for (let i = 0; i < out.length; i++) { out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16); } return out; }