import { describe, expect, test } from 'vitest'; import { Packet } from '../src/packet'; import { PayloadType, RouteType, NodeType, TracePayload, AdvertPayload, RequestPayload, TextPayload, ResponsePayload, RawCustomPayload, AnonReqPayload, Payload, AckPayload, PathPayload, GroupDataPayload, GroupTextPayload } from '../src/packet.types'; import { hexToBytes, bytesToHex } from '../src/parser'; describe('Packet.fromBytes', () => { test('frame 1: len=122 type=5 payload_len=99', () => { const hex = '1515747207E0B28A52BE12186BCCBCABFC88A0417BBF78D951FF9FEC725F90F032C0DC9B7FD27890228B926A90E317E089F948EC66D9EF01F0C8683B6B28EC1E2D053741A75E7EEF51047BB4C9A1FB6766B379024DBA80B8FEFE804FF9696209039C2388E461AA6138D1DF9FDD3E333E5DFC18660F3E05F3364E'; const bytes = hexToBytes(hex); expect(bytes.length).toBe(122); const pkt = Packet.fromBytes(bytes); expect(pkt.payload.length).toBe(99); expect(pkt.payloadType).toBe(PayloadType.GROUP_TEXT); const h = pkt.hash(); expect(h.toUpperCase()).toBe('A17FC3ECD23FCFAD'); }); test('frame 2: len=32 type=1 payload_len=20', () => { const hex = '050AA50E2CB0336DB67BBF78928A3BB9BF7A8B677C83B6EC0716F9DD10002A06'; const bytes = hexToBytes(hex); expect(bytes.length).toBe(32); const pkt = Packet.fromBytes(bytes); expect(pkt.payload.length).toBe(20); expect(pkt.payloadType).toBe(PayloadType.RESPONSE); expect(pkt.hash().toUpperCase()).toBe('1D378AD8B7EBA411'); }); test('frame 3: len=38 type=0 payload_len=20', () => { const hex = '01104070B0331D9F19E44D36D5EECBC1BF78E8895A088C823AC61263D635A0AE1CF0FFAFF185'; const bytes = hexToBytes(hex); expect(bytes.length).toBe(38); const pkt = Packet.fromBytes(bytes); expect(pkt.payload.length).toBe(20); expect(pkt.payloadType).toBe(PayloadType.REQUEST); expect(pkt.hash().toUpperCase()).toBe('9948A57E8507EB95'); }); test('frame 4: len=37 type=8 payload_len=20', () => { const hex = '210F95DE1A16E9726BBDAE4D36D5EEBF78B6C6157F5F75D077EA15FF2A7F4A354F12A7C7C5'; const bytes = hexToBytes(hex); expect(bytes.length).toBe(37); const pkt = Packet.fromBytes(bytes); expect(pkt.payload.length).toBe(20); expect(pkt.payloadType).toBe(PayloadType.PATH); expect(pkt.hash().toUpperCase()).toBe('0A5157C46F34ECC1'); }); test('frame 5: len=26 type=3 payload_len=20', () => { const hex = '2742FD6C4C3B1A35248B823B6CA2BAF2E93DC8F3B8A895ED868B68BFB04986C04E078166A7F5651F0872538123199FD4FE910948DA5361FF5E8CB5ACB1A2AC4220D2101FC9ACCB30B990D4EFC2C163B578BAAE15FF5DC216539648B87108764945DFC888BFC04F0C28B3410DF844993D8F23EF83DE4B131E52966C5F110F46'; const bytes = hexToBytes(hex); const pkt = Packet.fromBytes(bytes); expect(pkt.routeType).toBe(RouteType.TRANSPORT_DIRECT); expect(pkt.payloadType).toBe(PayloadType.TRACE); const payload = pkt.decode() as TracePayload; expect(payload.type).toBe(PayloadType.TRACE); // the TRACE payload format has been updated; ensure we decode a TRACE payload expect(payload.type).toBe(PayloadType.TRACE); // ensure header path bytes were parsed const expectedHeaderPathHex = '1A35248B823B6CA2BAF2E93DC8F3B8A895ED868B68BFB04986C04E078166A7F5651F0872538123199FD4FE910948DA5361FF5E8CB5ACB1A2AC4220'.toUpperCase(); expect(bytesToHex(pkt.path).toUpperCase()).toBe(expectedHeaderPathHex); // transport codes (big-endian words as parsed from the packet) expect(pkt.transport).toEqual([0x42fd, 0x6c4c]); expect(pkt.pathLength).toBe(0x3b); // payload bytes check (raw payload must match expected) const expectedPayloadHex = 'D2101FC9ACCB30B990D4EFC2C163B578BAAE15FF5DC216539648B87108764945DFC888BFC04F0C28B3410DF844993D8F23EF83DE4B131E52966C5F110F46'.toUpperCase(); expect(bytesToHex(pkt.payload).toUpperCase()).toBe(expectedPayloadHex); // verify decoded trace fields: tag, authCode, flags and nodes const trace = payload as TracePayload; // tag/auth are read as little-endian uint32 values (memcpy on little-endian C) expect(trace.tag).toBe(0xC91F10D2); expect(trace.authCode).toBe(0xB930CBAC); // expect(trace.flags).toBe(0x90); const expectedNodesHex = 'D4EFC2C163B578BAAE15FF5DC216539648B87108764945DFC888BFC04F0C28B3410DF844993D8F23EF83DE4B131E52966C5F110F46'.toUpperCase(); expect(bytesToHex(trace.nodes).toUpperCase()).toBe(expectedNodesHex); }); test('frame 6: len=110 type=1 payload_len=99', () => { const hex = '1102607BE88177A117AE4391668509349D30A76FBA92E90CB9B1A75F49AC3382FED4E773336056663D9B84598A431A9ABE05D4F5214DF358133D8EB7022B63B92829335E8D5742B3249477744411BDC1E6664D3BAAAF170E50DF91F07D6E68FAE8A34616030E8F0992143711038C3953004E4C2D4548562D564247422D52505452'; const bytes = hexToBytes(hex); const pkt = Packet.fromBytes(bytes); expect(pkt.routeType).toBe(RouteType.FLOOD); expect(pkt.payloadType).toBe(PayloadType.ADVERT); const adv = pkt.decode() as AdvertPayload; expect(adv.type).toBe(PayloadType.ADVERT); const pubHex = 'E88177A117AE4391668509349D30A76FBA92E90CB9B1A75F49AC3382FED4E773'; expect(bytesToHex(adv.publicKey).toUpperCase()).toBe(pubHex); // timestamp should match 2024-05-28T22:52:35Z expect(adv.timestamp.toISOString()).toBe('2024-05-28T22:52:35.000Z'); const sigHex = '3D9B84598A431A9ABE05D4F5214DF358133D8EB7022B63B92829335E8D5742B3249477744411BDC1E6664D3BAAAF170E50DF91F07D6E68FAE8A34616030E8F09'; expect(bytesToHex(adv.signature).toUpperCase()).toBe(sigHex); // appdata flags 0x92 -> nodeType 0x02 (REPEATER), hasLocation true, hasName true expect(adv.appdata.nodeType).toBe(NodeType.REPEATER); expect(adv.appdata.hasLocation).toBe(true); expect(adv.appdata.hasName).toBe(true); // location values: parser appears to scale values by 10 here, accept that expect(adv.appdata.location).toBeDefined(); expect(adv.appdata.location![0] / 10).toBeCloseTo(51.45986, 5); expect(adv.appdata.location![1] / 10).toBeCloseTo(5.45422, 5); expect(adv.appdata.name).toBe('NL-EHV-VBGB-RPTR'); expect(pkt.hash().toUpperCase()).toBe('67C10F75168ECC8C'); }); }); describe('Packet decode branches and transport/path parsing', () => { const makePacket = (payloadType: number, routeType: number, pathBytes: Uint8Array, payload: Uint8Array, transportWords?: [number, number]) => { const header = (0 << 6) | (payloadType << 2) | routeType; const parts: number[] = [header]; if (transportWords) { // big-endian uint16 x2 parts.push((transportWords[0] >> 8) & 0xff, transportWords[0] & 0xff); parts.push((transportWords[1] >> 8) & 0xff, transportWords[1] & 0xff); } const pathLength = pathBytes.length; parts.push(pathLength); const arr = new Uint8Array(parts.length + pathBytes.length + payload.length); arr.set(parts, 0); arr.set(pathBytes, parts.length); arr.set(payload, parts.length + pathBytes.length); return arr; }; test('hasTransportCodes true/false and transport parsed', () => { // transport present (route TRANSPORT_FLOOD = 0) const p = makePacket(PayloadType.REQUEST, RouteType.TRANSPORT_FLOOD, new Uint8Array([]), new Uint8Array([0,0,1,2]), [0x1122, 0x3344]); const pkt = Packet.fromBytes(p); expect(pkt.transport).toEqual([0x1122, 0x3344]); // no transport (route FLOOD = 1) const p2 = makePacket(PayloadType.REQUEST, RouteType.FLOOD, new Uint8Array([]), new Uint8Array([0,0,1,2])); const pkt2 = Packet.fromBytes(p2); expect(pkt2.transport).toBeUndefined(); }); test('payload REQUEST/RESPONSE/TEXT decode (encrypted parsing)', () => { const payload = new Uint8Array([0xAA, 0xBB, 0x01, 0x02, 0x03]); // dst,src, mac(2), cipherText(1) const pkt = Packet.fromBytes(makePacket(PayloadType.REQUEST, RouteType.DIRECT, new Uint8Array([]), payload)); const req = pkt.decode() as RequestPayload; expect(req.type).toBe(PayloadType.REQUEST); expect(req.dst).toBe(0xAA); expect(req.src).toBe(0xBB); const resp = Packet.fromBytes(makePacket(PayloadType.RESPONSE, RouteType.DIRECT, new Uint8Array([]), payload)).decode() as ResponsePayload; expect(resp.type).toBe(PayloadType.RESPONSE); const txt = Packet.fromBytes(makePacket(PayloadType.TEXT, RouteType.DIRECT, new Uint8Array([]), payload)).decode() as TextPayload; expect(txt.type).toBe(PayloadType.TEXT); }); test('ACK decode and RAW_CUSTOM', () => { const ackPayload = new Uint8Array([0x01,0x02,0x03,0x04]); const ack = Packet.fromBytes(makePacket(PayloadType.ACK, RouteType.DIRECT, new Uint8Array([]), ackPayload)).decode() as AckPayload; expect(ack.type).toBe(PayloadType.ACK); const custom = new Uint8Array([0x99,0x88,0x77]); const rc = Packet.fromBytes(makePacket(PayloadType.RAW_CUSTOM, RouteType.DIRECT, new Uint8Array([]), custom)).decode() as RawCustomPayload; expect(rc.type).toBe(PayloadType.RAW_CUSTOM); expect(rc.data).toEqual(custom); }); test('ADVERT minimal decode (no appdata extras)', () => { const publicKey = new Uint8Array(32).fill(1); const timestamp = new Uint8Array([0x01,0x00,0x00,0x00]); const signature = new Uint8Array(64).fill(2); const flags = new Uint8Array([0x00]); const payload = new Uint8Array([...publicKey, ...timestamp, ...signature, ...flags]); const pkt = Packet.fromBytes(makePacket(PayloadType.ADVERT, RouteType.DIRECT, new Uint8Array([]), payload)); const adv = pkt.decode() as AdvertPayload; expect(adv.type).toBe(PayloadType.ADVERT); expect(adv.publicKey.length).toBe(32); expect(adv.signature.length).toBe(64); expect(adv.appdata.hasName).toBe(false); }); test('GROUP_TEXT and GROUP_DATA decode', () => { const payload = new Uint8Array([0x55, 0x01, 0x02, 0x03]); // channelHash + mac(2) + cipher const gt = Packet.fromBytes(makePacket(PayloadType.GROUP_TEXT, RouteType.DIRECT, new Uint8Array([]), payload)).decode() as GroupTextPayload; expect(gt.type).toBe(PayloadType.GROUP_TEXT); const gd = Packet.fromBytes(makePacket(PayloadType.GROUP_DATA, RouteType.DIRECT, new Uint8Array([]), payload)).decode() as GroupDataPayload; expect(gd.type).toBe(PayloadType.GROUP_DATA); }); test('ANON_REQ decode', () => { const dst = 0x12; const pub = new Uint8Array(32).fill(3); const enc = new Uint8Array([0x01,0x02,0x03]); const payload = new Uint8Array([dst, ...pub, ...enc]); const ar = Packet.fromBytes(makePacket(PayloadType.ANON_REQ, RouteType.DIRECT, new Uint8Array([]), payload)).decode() as AnonReqPayload; expect(ar.type).toBe(PayloadType.ANON_REQ); expect(ar.dst).toBe(0x12); }); test('PATH and TRACE decode nodes', () => { const pathPayload = new Uint8Array([0x0a, 0x0b]); const path = Packet.fromBytes(makePacket(PayloadType.PATH, RouteType.DIRECT, new Uint8Array([]), pathPayload)).decode() as PathPayload; expect(path.type).toBe(PayloadType.PATH); const nodes = new Uint8Array([0x01,0x02,0x03]); // construct TRACE payload: tag (4 bytes LE), authCode (4 bytes LE), flags (1), nodes... const tag = new Uint8Array([0x01,0x00,0x00,0x00]); const auth = new Uint8Array([0x02,0x00,0x00,0x00]); const flags = new Uint8Array([0x00]); const tracePayload = new Uint8Array([...tag, ...auth, ...flags, ...nodes]); const trace = Packet.fromBytes(makePacket(PayloadType.TRACE, RouteType.DIRECT, new Uint8Array([]), tracePayload)).decode() as TracePayload; expect(trace.type).toBe(PayloadType.TRACE); expect(trace.nodes).toBeInstanceOf(Uint8Array); }); test('pathHashes parsing when multiple hashes', () => { // create pathLength byte: count=2 size=3 -> (1<<6)|3 = 67 const pathLengthByte = 67; const header = (0 << 6) | (PayloadType.RAW_CUSTOM << 2) | RouteType.DIRECT; const payload = new Uint8Array([0x01]); const pathBytes = new Uint8Array([0xAA,0xBB,0xCC, 0x11,0x22,0x33]); const parts: number[] = [header, pathLengthByte]; const arr = new Uint8Array(parts.length + pathBytes.length + payload.length); arr.set(parts, 0); arr.set(pathBytes, parts.length); arr.set(payload, parts.length + pathBytes.length); const pkt = Packet.fromBytes(arr); expect(pkt.pathHashCount).toBe(3); expect(pkt.pathHashSize).toBe(2); expect(pkt.pathHashes.length).toBe(3); expect(pkt.pathHashes[0]).toBe(bytesToHex(pathBytes.subarray(0,2))); }); test('unsupported payload type throws', () => { // payloadType 0x0a is not handled const header = (0 << 6) | (0x0a << 2) | RouteType.DIRECT; const arr = new Uint8Array([header, 0x00]); const pkt = Packet.fromBytes(arr); expect(() => pkt.decode()).toThrow(); }); }); describe("Packet.decode overloads", () => { const ackBytes = new Uint8Array([ /* header */ 13, /* pathLength */ 0, /* payload (4 bytes checksum) */ 1, 2, 3, 4 ]); test("decode() returns payload only", () => { const pkt = Packet.fromBytes(ackBytes); const payload = pkt.decode() as Payload; expect(payload.type).toBe(PayloadType.ACK); expect((payload as any).checksum).toEqual(new Uint8Array([1, 2, 3, 4])); }); test("decode(true) returns { payload, structure }", () => { const pkt = Packet.fromBytes(ackBytes); const res = pkt.decode(true) as any; expect(res).toHaveProperty("payload"); expect(res).toHaveProperty("structure"); expect(res.payload.type).toBe(PayloadType.ACK); expect(Array.isArray(res.structure)).toBe(true); expect(res.structure[res.structure.length - 1].name).toBe("ack payload"); }); });