Files
meshcore.ts/test/packet.test.ts
2026-03-12 20:56:04 +01:00

308 lines
14 KiB
TypeScript

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 { bytesToHex, Dissected, hexToBytes } from "@hamradio/packet";
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(5.145986, 5);
expect(adv.appdata.location![1] / 10).toBeCloseTo(0.545422, 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 AckPayload).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 unknown as { payload: Payload; structure: Dissected };
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");
});
});