From 7c2cc0e0f67cdaaa28e6a113eb6cb7179f557ac1 Mon Sep 17 00:00:00 2001 From: maze Date: Wed, 11 Mar 2026 13:23:52 +0100 Subject: [PATCH] Implemented Packet structure decoding --- README.md | 47 ++++++ package.json | 2 +- src/index.ts | 66 +++++--- src/packet.ts | 384 +++++++++++++++++++++++++++++++++++++------- src/packet.types.ts | 41 +++-- src/parser.types.ts | 35 ++++ test/packet.test.ts | 41 +++-- 7 files changed, 519 insertions(+), 97 deletions(-) create mode 100644 src/parser.types.ts diff --git a/README.md b/README.md index b4b90a5..043f69f 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,53 @@ _Packet { */ ``` +## Packet structure parsing + +The parser can also be instructed to generate a packet structure, useful for debugging or +printing packet details: + +```ts +import { Packet } from '@hamradio/meshcore'; + +const raw = new Uint8Array(Buffer.from("050AA50E2CB0336DB67BBF78928A3BB9BF7A8B677C83B6EC0716F9DD10002A06", "hex")); +const packet = Packet.fromBytes(raw); +const { structure } = packet.decode(true); +console.log(structure); + +/* +[ + { + name: 'header', + data: Uint8Array(12) [ + 5, 10, 165, 14, 44, + 176, 51, 109, 182, 123, + 191, 120 + ], + fields: [ + { name: 'flags', type: 0, size: 1, bits: [Array] }, + { name: 'path length', type: 1, size: 1, bits: [Array] }, + { name: 'path hashes', type: 6, size: 10 } + ] + }, + { + name: 'response payload', + data: Uint8Array(20) [ + 146, 138, 59, 185, 191, 122, + 139, 103, 124, 131, 182, 236, + 7, 22, 249, 221, 16, 0, + 42, 6 + ], + fields: [ + { name: 'destination hash', type: 1, size: 1, value: 146 }, + { name: 'source hash', type: 1, size: 1, value: 138 }, + { name: 'cipher MAC', type: 6, size: 2, value: [Uint8Array] }, + { name: 'cipher text', type: 6, size: 16, value: [Uint8Array] } + ] + } +] +*/ +``` + ## Identities The package supports: diff --git a/package.json b/package.json index a0ee468..b2d271d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hamradio/meshcore", - "version": "1.1.3", + "version": "1.1.4", "description": "MeshCore protocol support for Typescript", "keywords": [ "MeshCore", diff --git a/src/index.ts b/src/index.ts index 2b201a6..eb7e396 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,50 @@ -export * from './identity'; -import * as identityTypes from './identity.types'; -import type * as identityTypesTypes from './identity.types'; +export { + type IPacket, + type Payload, + type EncryptedPayload, + type RequestPayload, + type ResponsePayload, + type TextPayload, + type AckPayload, + type AdvertPayload, + type GroupTextPayload, + type GroupDataPayload, + type AnonReqPayload, + type PathPayload, + type TracePayload, + type RawCustomPayload, + RouteType, + PayloadType, + RequestType, + TextType, + NodeType, +} from "./packet.types"; +export { Packet } from "./packet"; -export * from './crypto'; -import * as cryptoTypes from './crypto.types'; -import type * as cryptoTypesTypes from './crypto.types'; +export { + type NodeHash, + type IIdentity, + type ILocalIdentity, + type IContact +} from "./identity.types"; +export { + parseNodeHash, + Identity, + LocalIdentity, + Contact, + Group, + Contacts +} from "./identity"; -export * from './packet'; -import * as packetTypes from './packet.types'; -import type * as packetTypesTypes from './packet.types'; - -export type { - identityTypes, - identityTypesTypes, - cryptoTypes, - cryptoTypesTypes, - packetTypes, - packetTypesTypes -}; +export { + type IPublicKey, + type IPrivateKey, + type ISharedSecret, + type IStaticSecret +} from "./crypto.types"; +export { + PublicKey, + PrivateKey, + SharedSecret, + StaticSecret, +} from "./crypto"; diff --git a/src/packet.ts b/src/packet.ts index 7533a52..4fac2b2 100644 --- a/src/packet.ts +++ b/src/packet.ts @@ -2,6 +2,7 @@ import { sha256 } from "@noble/hashes/sha2.js"; import { AckPayload, AdvertAppData, + AdvertFlag, AdvertPayload, AnonReqPayload, EncryptedPayload, @@ -24,6 +25,7 @@ import { BufferReader, bytesToHex } from "./parser"; +import { FieldType, PacketSegment, PacketStructure } from "./parser.types"; export class Packet implements IPacket { // Raw packet bytes. @@ -40,6 +42,8 @@ export class Packet implements IPacket { public pathHashSize: number; public pathHashBytes: number; public pathHashes: string[]; + // Parsed packet segments. + public structure?: PacketStructure | undefined; constructor(header: number, transport: [number, number] | undefined, pathLength: number, path: Uint8Array, payload: Uint8Array) { this.header = header; @@ -99,33 +103,117 @@ export class Packet implements IPacket { return bytesToHex(digest.slice(0, 8)); } - public decode(): Payload { + private ensureStructure(): void { + if (typeof this.structure !== "undefined") { + return; + } + + let pathHashType: FieldType + switch (this.pathHashSize) { + case 1: pathHashType = FieldType.BYTES; break; + case 2: pathHashType = FieldType.WORDS; break; + case 4: pathHashType = FieldType.DWORDS; break; + default: + throw new Error(`Unsupported path hash size: ${this.pathHashSize}`); + } + + this.structure = [ + /* Header segment */ + { name: "header", data: new Uint8Array([this.header, this.pathLength, ...this.path]), fields: [ + /* Header flags */ + { + name: "flags", + type: FieldType.BITS, + size: 1, + bits: [ + { name: "route type", size: 2 }, + { name: "payload version", size: 2 }, + { name: "payload type", size: 4 }, + ] + }, + /* Transport codes */ + ...(Packet.hasTransportCodes(this.routeType) ? [ + { + name: "transport code 1", + type: FieldType.UINT16_BE, + size: 2 + }, + { + name: "transport code 2", + type: FieldType.UINT16_BE, + size: 2 + }, + ] : []), + + /* Path length and hashes */ + { + name: "path length", + type: FieldType.UINT8, + size: 1, + bits: [ + { name: "path hash size", size: 2 }, + { name: "path hash count", size: 6 }, + ] + }, + { + name: "path hashes", + type: pathHashType, + size: this.path.length + } + ]}, + ] + } + + public decode(withStructure?: boolean): Payload | { payload: Payload, structure: PacketStructure } { + let result: Payload | { payload: Payload, segment: PacketSegment }; + switch (this.payloadType) { case PayloadType.REQUEST: - return this.decodeRequest(); + result = this.decodeRequest(withStructure); + break; case PayloadType.RESPONSE: - return this.decodeResponse(); + result = this.decodeResponse(withStructure); + break; case PayloadType.TEXT: - return this.decodeText(); + result = this.decodeText(withStructure); + break; case PayloadType.ACK: - return this.decodeAck(); + result = this.decodeAck(withStructure); + break; case PayloadType.ADVERT: - return this.decodeAdvert(); + result = this.decodeAdvert(withStructure); + break; case PayloadType.GROUP_TEXT: - return this.decodeGroupText(); + result = this.decodeGroupText(withStructure); + break; case PayloadType.GROUP_DATA: - return this.decodeGroupData(); + result = this.decodeGroupData(withStructure); + break; case PayloadType.ANON_REQ: - return this.decodeAnonReq(); + result = this.decodeAnonReq(withStructure); + break; case PayloadType.PATH: - return this.decodePath(); + result = this.decodePath(withStructure); + break; case PayloadType.TRACE: - return this.decodeTrace(); + result = this.decodeTrace(withStructure); + break; case PayloadType.RAW_CUSTOM: - return this.decodeRawCustom(); + result = this.decodeRawCustom(withStructure); + break; default: throw new Error(`Unsupported payload type: ${this.payloadType}`); } + + console.log('packet decode with structure:', typeof withStructure, withStructure, { result }); + + if (typeof withStructure === "boolean" && withStructure && "segment" in result && "payload" in result) { + this.ensureStructure(); + const structure = [ ...this.structure!, result.segment ]; + return { payload: result.payload, structure }; + } + + return result as Payload; } private decodeEncryptedPayload(reader: BufferReader): EncryptedPayload { @@ -134,61 +222,128 @@ export class Packet implements IPacket { return { cipherMAC, cipherText }; } - private decodeRequest(): RequestPayload { + private decodeRequest(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } { if (this.payload.length < 4) { throw new Error("Invalid request payload: too short"); } const reader = new BufferReader(this.payload); - return { - type: PayloadType.REQUEST, - dst: reader.readByte(), - src: reader.readByte(), - encrypted: this.decodeEncryptedPayload(reader), + const dst = reader.readByte(); + const src = reader.readByte(); + const encrypted = this.decodeEncryptedPayload(reader); + const payload: RequestPayload = { + type: PayloadType.REQUEST, + dst, + src, + encrypted, + }; + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "request payload", + data: this.payload, + fields: [ + { name: "destination hash", type: FieldType.UINT8, size: 1, value: dst }, + { name: "source hash", type: FieldType.UINT8, size: 1, value: src }, + { name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC }, + { name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText } + ] + } + return { payload, segment }; } + return payload; } - private decodeResponse(): ResponsePayload { + private decodeResponse(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } { if (this.payload.length < 4) { throw new Error("Invalid response payload: too short"); } const reader = new BufferReader(this.payload); - return { + const dst = reader.readByte(); + const src = reader.readByte(); + const encrypted = this.decodeEncryptedPayload(reader); + const payload: ResponsePayload = { type: PayloadType.RESPONSE, - dst: reader.readByte(), - src: reader.readByte(), - encrypted: this.decodeEncryptedPayload(reader), + dst, + src, + encrypted, + }; + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "response payload", + data: this.payload, + fields: [ + { name: "destination hash", type: FieldType.UINT8, size: 1, value: dst }, + { name: "source hash", type: FieldType.UINT8, size: 1, value: src }, + { name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC }, + { name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText } + ] + }; + return { payload, segment }; } + return payload; } - private decodeText(): TextPayload { + private decodeText(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } { if (this.payload.length < 4) { throw new Error("Invalid text payload: too short"); } const reader = new BufferReader(this.payload); - return { + const dst = reader.readByte(); + const src = reader.readByte(); + const encrypted = this.decodeEncryptedPayload(reader); + const payload: TextPayload = { type: PayloadType.TEXT, - dst: reader.readByte(), - src: reader.readByte(), - encrypted: this.decodeEncryptedPayload(reader), + dst, + src, + encrypted, + }; + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "text payload", + data: this.payload, + fields: [ + { name: "destination hash", type: FieldType.UINT8, size: 1, value: dst }, + { name: "source hash", type: FieldType.UINT8, size: 1, value: src }, + { name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC }, + { name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText } + ] + }; + return { payload, segment }; } + return payload; } - private decodeAck(): AckPayload { + private decodeAck(withSegment?: boolean): Payload | { payload: AckPayload, segment: PacketSegment } { if (this.payload.length < 4) { throw new Error("Invalid ack payload: too short"); } const reader = new BufferReader(this.payload); - return { + const checksum = reader.readBytes(4); + const payload: AckPayload = { type: PayloadType.ACK, - checksum: reader.readBytes(4), + checksum, + }; + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "ack payload", + data: this.payload, + fields: [ + { name: "checksum", type: FieldType.BYTES, size: 4, value: checksum } + ] + }; + return { payload, segment }; } + return payload; } - private decodeAdvert(): AdvertPayload { + private decodeAdvert(withSegment?: boolean): Payload | { payload: AdvertPayload, segment: PacketSegment } { if (this.payload.length < 4) { throw new Error("Invalid advert payload: too short"); } @@ -201,25 +356,57 @@ export class Packet implements IPacket { signature: reader.readBytes(64), } + let segment: PacketSegment | undefined; + if (typeof withSegment === "boolean" && withSegment) { + segment = { + name: "advert payload", + data: this.payload, + fields: [ + { type: FieldType.BYTES, name: "public key", size: 32 }, + { type: FieldType.UINT32_LE, name: "timestamp", size: 4, value: payload.timestamp! }, + { type: FieldType.BYTES, name: "signature", size: 64 }, + ] + }; + } + const flags = reader.readByte(); const appdata: AdvertAppData = { nodeType: flags & 0x0f, - hasLocation: (flags & 0x10) !== 0, - hasFeature1: (flags & 0x20) !== 0, - hasFeature2: (flags & 0x40) !== 0, - hasName: (flags & 0x80) !== 0, + hasLocation: (flags & AdvertFlag.HAS_LOCATION) !== 0, + hasFeature1: (flags & AdvertFlag.HAS_FEATURE1) !== 0, + hasFeature2: (flags & AdvertFlag.HAS_FEATURE2) !== 0, + hasName: (flags & AdvertFlag.HAS_NAME) !== 0, + } + if (typeof withSegment === "boolean" && withSegment) { + segment!.fields.push({ type: FieldType.BITS, name: "flags", size: 1, value: flags, bits: [ + { size: 4, name: "node type" }, + { size: 1, name: "location flag" }, + { size: 1, name: "feature1 flag" }, + { size: 1, name: "feature2 flag" }, + { size: 1, name: "name flag" }, + ]}); } if (appdata.hasLocation) { const lat = reader.readInt32LE() / 100000; const lon = reader.readInt32LE() / 100000; appdata.location = [lat, lon]; + if (typeof withSegment === "boolean" && withSegment) { + segment!.fields.push({ type: FieldType.UINT32_LE, name: "latitude", size: 4, value: lat }); + segment!.fields.push({ type: FieldType.UINT32_LE, name: "longitude", size: 4, value: lon }); + } } if (appdata.hasFeature1) { appdata.feature1 = reader.readUint16LE(); + if (typeof withSegment === "boolean" && withSegment) { + segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature1", size: 2, value: appdata.feature1 }); + } } if (appdata.hasFeature2) { appdata.feature2 = reader.readUint16LE(); + if (typeof withSegment === "boolean" && withSegment) { + segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature2", size: 2, value: appdata.feature2 }); + } } if (appdata.hasName) { const nameBytes = reader.readBytes(); @@ -228,86 +415,175 @@ export class Packet implements IPacket { nullPos = nameBytes.length; } appdata.name = new TextDecoder('utf-8').decode(nameBytes.subarray(0, nullPos)); + if (typeof withSegment === "boolean" && withSegment) { + segment!.fields.push({ type: FieldType.C_STRING, name: "name", size: nameBytes.length, value: appdata.name }); + } } - return { - ...payload, - appdata - } as AdvertPayload; + if (typeof withSegment === "boolean" && withSegment && typeof segment !== "undefined") { + return { payload: { ...payload, appdata } as AdvertPayload, segment }; + } + + return { ...payload, appdata } as AdvertPayload; } - private decodeGroupText(): GroupTextPayload { + private decodeGroupText(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } { if (this.payload.length < 3) { throw new Error("Invalid group text payload: too short"); } const reader = new BufferReader(this.payload); - return { + const channelHash = reader.readByte(); + const encrypted = this.decodeEncryptedPayload(reader); + const payload: GroupTextPayload = { type: PayloadType.GROUP_TEXT, - channelHash: reader.readByte(), - encrypted: this.decodeEncryptedPayload(reader), + channelHash, + encrypted, + }; + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "group text payload", + data: this.payload, + fields: [ + { name: "channel hash", type: FieldType.UINT8, size: 1, value: channelHash }, + { name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC }, + { name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText } + ] + }; + return { payload, segment }; } + return payload; } - private decodeGroupData(): GroupDataPayload { + private decodeGroupData(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } { if (this.payload.length < 3) { throw new Error("Invalid group data payload: too short"); } const reader = new BufferReader(this.payload); - return { + const payload: GroupDataPayload = { type: PayloadType.GROUP_DATA, channelHash: reader.readByte(), encrypted: this.decodeEncryptedPayload(reader), + }; + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "group data payload", + data: this.payload, + fields: [ + { name: "channel hash", type: FieldType.UINT8, size: 1, value: payload.channelHash }, + { name: "cipher MAC", type: FieldType.BYTES, size: 2, value: payload.encrypted.cipherMAC }, + { name: "cipher text", type: FieldType.BYTES, size: payload.encrypted.cipherText.length, value: payload.encrypted.cipherText } + ] + }; + return { payload, segment }; } + return payload; } - private decodeAnonReq(): AnonReqPayload { + private decodeAnonReq(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } { if (this.payload.length < 1 + 32 + 2) { throw new Error("Invalid anon req payload: too short"); } const reader = new BufferReader(this.payload); - return { + const payload: AnonReqPayload = { type: PayloadType.ANON_REQ, dst: reader.readByte(), publicKey: reader.readBytes(32), encrypted: this.decodeEncryptedPayload(reader), } + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "anon req payload", + data: this.payload, + fields: [ + { name: "destination hash", type: FieldType.UINT8, size: 1, value: payload.dst }, + { name: "public key", type: FieldType.BYTES, size: 32, value: payload.publicKey }, + { name: "cipher MAC", type: FieldType.BYTES, size: 2, value: payload.encrypted.cipherMAC }, + { name: "cipher text", type: FieldType.BYTES, size: payload.encrypted.cipherText.length, value: payload.encrypted.cipherText } + ] + }; + return { payload, segment }; + } + return payload; } - private decodePath(): PathPayload { + private decodePath(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } { if (this.payload.length < 2) { throw new Error("Invalid path payload: too short"); } const reader = new BufferReader(this.payload); - return { + const payload: PathPayload = { type: PayloadType.PATH, dst: reader.readByte(), src: reader.readByte(), + }; + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "path payload", + data: this.payload, + fields: [ + { name: "destination hash", type: FieldType.UINT8, size: 1, value: payload.dst }, + { name: "source hash", type: FieldType.UINT8, size: 1, value: payload.src } + ] + }; + return { payload, segment }; } + return payload; } - private decodeTrace(): TracePayload { + private decodeTrace(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } { if (this.payload.length < 9) { throw new Error("Invalid trace payload: too short"); } const reader = new BufferReader(this.payload); - return { + const payload: TracePayload = { type: PayloadType.TRACE, tag: reader.readUint32LE() >>> 0, authCode: reader.readUint32LE() >>> 0, flags: reader.readByte() & 0x03, nodes: reader.readBytes() + }; + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "trace payload", + data: this.payload, + fields: [ + { name: "tag", type: FieldType.DWORDS, size: 4, value: payload.tag }, + { name: "auth code", type: FieldType.DWORDS, size: 4, value: payload.authCode }, + { name: "flags", type: FieldType.UINT8, size: 1, value: payload.flags }, + { name: "nodes", type: FieldType.BYTES, size: payload.nodes.length, value: payload.nodes } + ] + }; + return { payload, segment }; } + return payload; } - private decodeRawCustom(): RawCustomPayload { - return { + private decodeRawCustom(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } { + const payload: RawCustomPayload = { type: PayloadType.RAW_CUSTOM, data: this.payload, + }; + + if (typeof withSegment === "boolean" && withSegment) { + const segment = { + name: "raw custom payload", + data: this.payload, + fields: [ + { name: "data", type: FieldType.BYTES, size: this.payload.length, value: this.payload } + ] + }; + return { payload, segment }; } + return payload; } } diff --git a/src/packet.types.ts b/src/packet.types.ts index 2790689..50a3ae9 100644 --- a/src/packet.types.ts +++ b/src/packet.types.ts @@ -1,4 +1,5 @@ import { NodeHash } from "./identity.types"; +import { PacketStructure } from "./parser.types"; // IPacket contains the raw packet bytes. export type Uint16 = number; // 0..65535 @@ -12,7 +13,7 @@ export interface IPacket { path: Uint8Array; payload: Uint8Array; - decode(): Payload; + decode(withStructure?: boolean): Payload | { payload: Payload, structure: PacketStructure } } export enum RouteType { @@ -36,7 +37,7 @@ export enum PayloadType { RAW_CUSTOM = 0x0f, } -export type Payload = +export type Payload = BasePayload & ( | RequestPayload | ResponsePayload | TextPayload @@ -47,14 +48,19 @@ export type Payload = | AnonReqPayload | PathPayload | TracePayload - | RawCustomPayload; + | RawCustomPayload +); + +export interface BasePayload { + type: PayloadType; +} export interface EncryptedPayload { cipherMAC: Uint8Array; cipherText: Uint8Array; } -export interface RequestPayload { +export interface RequestPayload extends BasePayload { type: PayloadType.REQUEST; dst: NodeHash; src: NodeHash; @@ -78,7 +84,7 @@ export interface DecryptedRequest { requestData: Uint8Array; } -export interface ResponsePayload { +export interface ResponsePayload extends BasePayload { type: PayloadType.RESPONSE; dst: NodeHash; src: NodeHash; @@ -91,7 +97,7 @@ export interface DecryptedResponse { responseData: Uint8Array; } -export interface TextPayload { +export interface TextPayload extends BasePayload { type: PayloadType.TEXT; dst: NodeHash; src: NodeHash; @@ -112,12 +118,12 @@ export interface DecryptedText { message: string; } -export interface AckPayload { +export interface AckPayload extends BasePayload { type: PayloadType.ACK; checksum: Uint8Array; } -export interface AdvertPayload { +export interface AdvertPayload extends BasePayload { type: PayloadType.ADVERT; publicKey: Uint8Array; timestamp: Date; @@ -132,6 +138,13 @@ export enum NodeType { SENSOR_NODE = 0x04, } +export enum AdvertFlag { + HAS_LOCATION = 0x10, + HAS_FEATURE1 = 0x20, + HAS_FEATURE2 = 0x40, + HAS_NAME = 0x80, +} + export interface AdvertAppData { nodeType: NodeType; hasLocation: boolean; @@ -144,7 +157,7 @@ export interface AdvertAppData { name?: string; } -export interface GroupTextPayload { +export interface GroupTextPayload extends BasePayload { type: PayloadType.GROUP_TEXT; channelHash: NodeHash; encrypted: EncryptedPayload; @@ -158,7 +171,7 @@ export interface DecryptedGroupText { message: string; } -export interface GroupDataPayload { +export interface GroupDataPayload extends BasePayload { type: PayloadType.GROUP_DATA; channelHash: NodeHash; encrypted: EncryptedPayload; @@ -170,7 +183,7 @@ export interface DecryptedGroupData { data: Uint8Array; } -export interface AnonReqPayload { +export interface AnonReqPayload extends BasePayload { type: PayloadType.ANON_REQ; dst: NodeHash; publicKey: Uint8Array; @@ -183,13 +196,13 @@ export interface DecryptedAnonReq { data: Uint8Array; } -export interface PathPayload { +export interface PathPayload extends BasePayload { type: PayloadType.PATH; dst: NodeHash; src: NodeHash; } -export interface TracePayload { +export interface TracePayload extends BasePayload { type: PayloadType.TRACE; tag: number; authCode: number; @@ -197,7 +210,7 @@ export interface TracePayload { nodes: Uint8Array; } -export interface RawCustomPayload { +export interface RawCustomPayload extends BasePayload { type: PayloadType.RAW_CUSTOM; data: Uint8Array; } diff --git a/src/parser.types.ts b/src/parser.types.ts new file mode 100644 index 0000000..a42043f --- /dev/null +++ b/src/parser.types.ts @@ -0,0 +1,35 @@ +export enum FieldType { + BITS = 0, + UINT8 = 1, + UINT16_LE = 2, + UINT16_BE = 3, + UINT32_LE = 4, + UINT32_BE = 5, + BYTES = 6, // 8-bits per value + WORDS = 7, // 16-bits per value + DWORDS = 8, // 32-bits per value + QWORDS = 9, // 64-bits per value + C_STRING = 10, +} + +// Interface for the parsed packet segments, used for debugging and testing. +export type PacketStructure = PacketSegment[]; + +export interface PacketSegment { + name: string; + data: Uint8Array; + fields: PacketField[]; +} + +export interface PacketField { + type: FieldType; + size: number; // Size in bytes + name?: string; + bits?: PacketFieldBit[]; // Only for bit fields in FieldType.BITS + value?: any; // Optional decoded value +} + +export interface PacketFieldBit { + name: string; + size: number; // Size in bits +} diff --git a/test/packet.test.ts b/test/packet.test.ts index 584893b..9459726 100644 --- a/test/packet.test.ts +++ b/test/packet.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; import { Packet } from '../src/packet'; -import { PayloadType, RouteType, NodeType, TracePayload, AdvertPayload, RequestPayload, TextPayload, ResponsePayload, RawCustomPayload, AnonReqPayload } from '../src/packet.types'; +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', () => { @@ -51,7 +51,7 @@ describe('Packet.fromBytes', () => { const pkt = Packet.fromBytes(bytes); expect(pkt.routeType).toBe(RouteType.TRANSPORT_DIRECT); expect(pkt.payloadType).toBe(PayloadType.TRACE); - const payload = pkt.decode(); + 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); @@ -148,7 +148,7 @@ describe('Packet decode branches and transport/path parsing', () => { 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(); + 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]); @@ -173,9 +173,9 @@ describe('Packet decode branches and transport/path parsing', () => { 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(); + 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(); + const gd = Packet.fromBytes(makePacket(PayloadType.GROUP_DATA, RouteType.DIRECT, new Uint8Array([]), payload)).decode() as GroupDataPayload; expect(gd.type).toBe(PayloadType.GROUP_DATA); }); @@ -191,7 +191,7 @@ describe('Packet decode branches and transport/path parsing', () => { 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(); + 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]); @@ -217,10 +217,10 @@ describe('Packet decode branches and transport/path parsing', () => { arr.set(pathBytes, parts.length); arr.set(payload, parts.length + pathBytes.length); const pkt = Packet.fromBytes(arr); - expect(pkt.pathHashCount).toBe(2); - expect(pkt.pathHashSize).toBe(3); - expect(pkt.pathHashes.length).toBe(2); - expect(pkt.pathHashes[0]).toBe(bytesToHex(pathBytes.subarray(0,3))); + 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', () => { @@ -231,3 +231,24 @@ describe('Packet decode branches and transport/path parsing', () => { 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"); + }); +});