Implemented Packet structure decoding
This commit is contained in:
47
README.md
47
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
|
## Identities
|
||||||
|
|
||||||
The package supports:
|
The package supports:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hamradio/meshcore",
|
"name": "@hamradio/meshcore",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"description": "MeshCore protocol support for Typescript",
|
"description": "MeshCore protocol support for Typescript",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"MeshCore",
|
"MeshCore",
|
||||||
|
|||||||
66
src/index.ts
66
src/index.ts
@@ -1,20 +1,50 @@
|
|||||||
export * from './identity';
|
export {
|
||||||
import * as identityTypes from './identity.types';
|
type IPacket,
|
||||||
import type * as identityTypesTypes from './identity.types';
|
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';
|
export {
|
||||||
import * as cryptoTypes from './crypto.types';
|
type NodeHash,
|
||||||
import type * as cryptoTypesTypes from './crypto.types';
|
type IIdentity,
|
||||||
|
type ILocalIdentity,
|
||||||
|
type IContact
|
||||||
|
} from "./identity.types";
|
||||||
|
export {
|
||||||
|
parseNodeHash,
|
||||||
|
Identity,
|
||||||
|
LocalIdentity,
|
||||||
|
Contact,
|
||||||
|
Group,
|
||||||
|
Contacts
|
||||||
|
} from "./identity";
|
||||||
|
|
||||||
export * from './packet';
|
export {
|
||||||
import * as packetTypes from './packet.types';
|
type IPublicKey,
|
||||||
import type * as packetTypesTypes from './packet.types';
|
type IPrivateKey,
|
||||||
|
type ISharedSecret,
|
||||||
export type {
|
type IStaticSecret
|
||||||
identityTypes,
|
} from "./crypto.types";
|
||||||
identityTypesTypes,
|
export {
|
||||||
cryptoTypes,
|
PublicKey,
|
||||||
cryptoTypesTypes,
|
PrivateKey,
|
||||||
packetTypes,
|
SharedSecret,
|
||||||
packetTypesTypes
|
StaticSecret,
|
||||||
};
|
} from "./crypto";
|
||||||
|
|||||||
384
src/packet.ts
384
src/packet.ts
@@ -2,6 +2,7 @@ import { sha256 } from "@noble/hashes/sha2.js";
|
|||||||
import {
|
import {
|
||||||
AckPayload,
|
AckPayload,
|
||||||
AdvertAppData,
|
AdvertAppData,
|
||||||
|
AdvertFlag,
|
||||||
AdvertPayload,
|
AdvertPayload,
|
||||||
AnonReqPayload,
|
AnonReqPayload,
|
||||||
EncryptedPayload,
|
EncryptedPayload,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
BufferReader,
|
BufferReader,
|
||||||
bytesToHex
|
bytesToHex
|
||||||
} from "./parser";
|
} from "./parser";
|
||||||
|
import { FieldType, PacketSegment, PacketStructure } from "./parser.types";
|
||||||
|
|
||||||
export class Packet implements IPacket {
|
export class Packet implements IPacket {
|
||||||
// Raw packet bytes.
|
// Raw packet bytes.
|
||||||
@@ -40,6 +42,8 @@ export class Packet implements IPacket {
|
|||||||
public pathHashSize: number;
|
public pathHashSize: number;
|
||||||
public pathHashBytes: number;
|
public pathHashBytes: number;
|
||||||
public pathHashes: string[];
|
public pathHashes: string[];
|
||||||
|
// Parsed packet segments.
|
||||||
|
public structure?: PacketStructure | undefined;
|
||||||
|
|
||||||
constructor(header: number, transport: [number, number] | undefined, pathLength: number, path: Uint8Array, payload: Uint8Array) {
|
constructor(header: number, transport: [number, number] | undefined, pathLength: number, path: Uint8Array, payload: Uint8Array) {
|
||||||
this.header = header;
|
this.header = header;
|
||||||
@@ -99,33 +103,117 @@ export class Packet implements IPacket {
|
|||||||
return bytesToHex(digest.slice(0, 8));
|
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) {
|
switch (this.payloadType) {
|
||||||
case PayloadType.REQUEST:
|
case PayloadType.REQUEST:
|
||||||
return this.decodeRequest();
|
result = this.decodeRequest(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.RESPONSE:
|
case PayloadType.RESPONSE:
|
||||||
return this.decodeResponse();
|
result = this.decodeResponse(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.TEXT:
|
case PayloadType.TEXT:
|
||||||
return this.decodeText();
|
result = this.decodeText(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.ACK:
|
case PayloadType.ACK:
|
||||||
return this.decodeAck();
|
result = this.decodeAck(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.ADVERT:
|
case PayloadType.ADVERT:
|
||||||
return this.decodeAdvert();
|
result = this.decodeAdvert(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.GROUP_TEXT:
|
case PayloadType.GROUP_TEXT:
|
||||||
return this.decodeGroupText();
|
result = this.decodeGroupText(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.GROUP_DATA:
|
case PayloadType.GROUP_DATA:
|
||||||
return this.decodeGroupData();
|
result = this.decodeGroupData(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.ANON_REQ:
|
case PayloadType.ANON_REQ:
|
||||||
return this.decodeAnonReq();
|
result = this.decodeAnonReq(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.PATH:
|
case PayloadType.PATH:
|
||||||
return this.decodePath();
|
result = this.decodePath(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.TRACE:
|
case PayloadType.TRACE:
|
||||||
return this.decodeTrace();
|
result = this.decodeTrace(withStructure);
|
||||||
|
break;
|
||||||
case PayloadType.RAW_CUSTOM:
|
case PayloadType.RAW_CUSTOM:
|
||||||
return this.decodeRawCustom();
|
result = this.decodeRawCustom(withStructure);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported payload type: ${this.payloadType}`);
|
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 {
|
private decodeEncryptedPayload(reader: BufferReader): EncryptedPayload {
|
||||||
@@ -134,61 +222,128 @@ export class Packet implements IPacket {
|
|||||||
return { cipherMAC, cipherText };
|
return { cipherMAC, cipherText };
|
||||||
}
|
}
|
||||||
|
|
||||||
private decodeRequest(): RequestPayload {
|
private decodeRequest(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
|
||||||
if (this.payload.length < 4) {
|
if (this.payload.length < 4) {
|
||||||
throw new Error("Invalid request payload: too short");
|
throw new Error("Invalid request payload: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BufferReader(this.payload);
|
const reader = new BufferReader(this.payload);
|
||||||
return {
|
const dst = reader.readByte();
|
||||||
type: PayloadType.REQUEST,
|
const src = reader.readByte();
|
||||||
dst: reader.readByte(),
|
const encrypted = this.decodeEncryptedPayload(reader);
|
||||||
src: reader.readByte(),
|
const payload: RequestPayload = {
|
||||||
encrypted: this.decodeEncryptedPayload(reader),
|
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) {
|
if (this.payload.length < 4) {
|
||||||
throw new Error("Invalid response payload: too short");
|
throw new Error("Invalid response payload: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BufferReader(this.payload);
|
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,
|
type: PayloadType.RESPONSE,
|
||||||
dst: reader.readByte(),
|
dst,
|
||||||
src: reader.readByte(),
|
src,
|
||||||
encrypted: this.decodeEncryptedPayload(reader),
|
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) {
|
if (this.payload.length < 4) {
|
||||||
throw new Error("Invalid text payload: too short");
|
throw new Error("Invalid text payload: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BufferReader(this.payload);
|
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,
|
type: PayloadType.TEXT,
|
||||||
dst: reader.readByte(),
|
dst,
|
||||||
src: reader.readByte(),
|
src,
|
||||||
encrypted: this.decodeEncryptedPayload(reader),
|
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) {
|
if (this.payload.length < 4) {
|
||||||
throw new Error("Invalid ack payload: too short");
|
throw new Error("Invalid ack payload: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BufferReader(this.payload);
|
const reader = new BufferReader(this.payload);
|
||||||
return {
|
const checksum = reader.readBytes(4);
|
||||||
|
const payload: AckPayload = {
|
||||||
type: PayloadType.ACK,
|
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) {
|
if (this.payload.length < 4) {
|
||||||
throw new Error("Invalid advert payload: too short");
|
throw new Error("Invalid advert payload: too short");
|
||||||
}
|
}
|
||||||
@@ -201,25 +356,57 @@ export class Packet implements IPacket {
|
|||||||
signature: reader.readBytes(64),
|
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 flags = reader.readByte();
|
||||||
const appdata: AdvertAppData = {
|
const appdata: AdvertAppData = {
|
||||||
nodeType: flags & 0x0f,
|
nodeType: flags & 0x0f,
|
||||||
hasLocation: (flags & 0x10) !== 0,
|
hasLocation: (flags & AdvertFlag.HAS_LOCATION) !== 0,
|
||||||
hasFeature1: (flags & 0x20) !== 0,
|
hasFeature1: (flags & AdvertFlag.HAS_FEATURE1) !== 0,
|
||||||
hasFeature2: (flags & 0x40) !== 0,
|
hasFeature2: (flags & AdvertFlag.HAS_FEATURE2) !== 0,
|
||||||
hasName: (flags & 0x80) !== 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) {
|
if (appdata.hasLocation) {
|
||||||
const lat = reader.readInt32LE() / 100000;
|
const lat = reader.readInt32LE() / 100000;
|
||||||
const lon = reader.readInt32LE() / 100000;
|
const lon = reader.readInt32LE() / 100000;
|
||||||
appdata.location = [lat, lon];
|
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) {
|
if (appdata.hasFeature1) {
|
||||||
appdata.feature1 = reader.readUint16LE();
|
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) {
|
if (appdata.hasFeature2) {
|
||||||
appdata.feature2 = reader.readUint16LE();
|
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) {
|
if (appdata.hasName) {
|
||||||
const nameBytes = reader.readBytes();
|
const nameBytes = reader.readBytes();
|
||||||
@@ -228,86 +415,175 @@ export class Packet implements IPacket {
|
|||||||
nullPos = nameBytes.length;
|
nullPos = nameBytes.length;
|
||||||
}
|
}
|
||||||
appdata.name = new TextDecoder('utf-8').decode(nameBytes.subarray(0, nullPos));
|
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 {
|
if (typeof withSegment === "boolean" && withSegment && typeof segment !== "undefined") {
|
||||||
...payload,
|
return { payload: { ...payload, appdata } as AdvertPayload, segment };
|
||||||
appdata
|
}
|
||||||
} as AdvertPayload;
|
|
||||||
|
return { ...payload, appdata } as AdvertPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
private decodeGroupText(): GroupTextPayload {
|
private decodeGroupText(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
|
||||||
if (this.payload.length < 3) {
|
if (this.payload.length < 3) {
|
||||||
throw new Error("Invalid group text payload: too short");
|
throw new Error("Invalid group text payload: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BufferReader(this.payload);
|
const reader = new BufferReader(this.payload);
|
||||||
return {
|
const channelHash = reader.readByte();
|
||||||
|
const encrypted = this.decodeEncryptedPayload(reader);
|
||||||
|
const payload: GroupTextPayload = {
|
||||||
type: PayloadType.GROUP_TEXT,
|
type: PayloadType.GROUP_TEXT,
|
||||||
channelHash: reader.readByte(),
|
channelHash,
|
||||||
encrypted: this.decodeEncryptedPayload(reader),
|
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) {
|
if (this.payload.length < 3) {
|
||||||
throw new Error("Invalid group data payload: too short");
|
throw new Error("Invalid group data payload: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BufferReader(this.payload);
|
const reader = new BufferReader(this.payload);
|
||||||
return {
|
const payload: GroupDataPayload = {
|
||||||
type: PayloadType.GROUP_DATA,
|
type: PayloadType.GROUP_DATA,
|
||||||
channelHash: reader.readByte(),
|
channelHash: reader.readByte(),
|
||||||
encrypted: this.decodeEncryptedPayload(reader),
|
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) {
|
if (this.payload.length < 1 + 32 + 2) {
|
||||||
throw new Error("Invalid anon req payload: too short");
|
throw new Error("Invalid anon req payload: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BufferReader(this.payload);
|
const reader = new BufferReader(this.payload);
|
||||||
return {
|
const payload: AnonReqPayload = {
|
||||||
type: PayloadType.ANON_REQ,
|
type: PayloadType.ANON_REQ,
|
||||||
dst: reader.readByte(),
|
dst: reader.readByte(),
|
||||||
publicKey: reader.readBytes(32),
|
publicKey: reader.readBytes(32),
|
||||||
encrypted: this.decodeEncryptedPayload(reader),
|
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) {
|
if (this.payload.length < 2) {
|
||||||
throw new Error("Invalid path payload: too short");
|
throw new Error("Invalid path payload: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BufferReader(this.payload);
|
const reader = new BufferReader(this.payload);
|
||||||
return {
|
const payload: PathPayload = {
|
||||||
type: PayloadType.PATH,
|
type: PayloadType.PATH,
|
||||||
dst: reader.readByte(),
|
dst: reader.readByte(),
|
||||||
src: 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) {
|
if (this.payload.length < 9) {
|
||||||
throw new Error("Invalid trace payload: too short");
|
throw new Error("Invalid trace payload: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BufferReader(this.payload);
|
const reader = new BufferReader(this.payload);
|
||||||
return {
|
const payload: TracePayload = {
|
||||||
type: PayloadType.TRACE,
|
type: PayloadType.TRACE,
|
||||||
tag: reader.readUint32LE() >>> 0,
|
tag: reader.readUint32LE() >>> 0,
|
||||||
authCode: reader.readUint32LE() >>> 0,
|
authCode: reader.readUint32LE() >>> 0,
|
||||||
flags: reader.readByte() & 0x03,
|
flags: reader.readByte() & 0x03,
|
||||||
nodes: reader.readBytes()
|
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 {
|
private decodeRawCustom(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
|
||||||
return {
|
const payload: RawCustomPayload = {
|
||||||
type: PayloadType.RAW_CUSTOM,
|
type: PayloadType.RAW_CUSTOM,
|
||||||
data: this.payload,
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NodeHash } from "./identity.types";
|
import { NodeHash } from "./identity.types";
|
||||||
|
import { PacketStructure } from "./parser.types";
|
||||||
|
|
||||||
// IPacket contains the raw packet bytes.
|
// IPacket contains the raw packet bytes.
|
||||||
export type Uint16 = number; // 0..65535
|
export type Uint16 = number; // 0..65535
|
||||||
@@ -12,7 +13,7 @@ export interface IPacket {
|
|||||||
path: Uint8Array;
|
path: Uint8Array;
|
||||||
payload: Uint8Array;
|
payload: Uint8Array;
|
||||||
|
|
||||||
decode(): Payload;
|
decode(withStructure?: boolean): Payload | { payload: Payload, structure: PacketStructure }
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RouteType {
|
export enum RouteType {
|
||||||
@@ -36,7 +37,7 @@ export enum PayloadType {
|
|||||||
RAW_CUSTOM = 0x0f,
|
RAW_CUSTOM = 0x0f,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Payload =
|
export type Payload = BasePayload & (
|
||||||
| RequestPayload
|
| RequestPayload
|
||||||
| ResponsePayload
|
| ResponsePayload
|
||||||
| TextPayload
|
| TextPayload
|
||||||
@@ -47,14 +48,19 @@ export type Payload =
|
|||||||
| AnonReqPayload
|
| AnonReqPayload
|
||||||
| PathPayload
|
| PathPayload
|
||||||
| TracePayload
|
| TracePayload
|
||||||
| RawCustomPayload;
|
| RawCustomPayload
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface BasePayload {
|
||||||
|
type: PayloadType;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EncryptedPayload {
|
export interface EncryptedPayload {
|
||||||
cipherMAC: Uint8Array;
|
cipherMAC: Uint8Array;
|
||||||
cipherText: Uint8Array;
|
cipherText: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestPayload {
|
export interface RequestPayload extends BasePayload {
|
||||||
type: PayloadType.REQUEST;
|
type: PayloadType.REQUEST;
|
||||||
dst: NodeHash;
|
dst: NodeHash;
|
||||||
src: NodeHash;
|
src: NodeHash;
|
||||||
@@ -78,7 +84,7 @@ export interface DecryptedRequest {
|
|||||||
requestData: Uint8Array;
|
requestData: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResponsePayload {
|
export interface ResponsePayload extends BasePayload {
|
||||||
type: PayloadType.RESPONSE;
|
type: PayloadType.RESPONSE;
|
||||||
dst: NodeHash;
|
dst: NodeHash;
|
||||||
src: NodeHash;
|
src: NodeHash;
|
||||||
@@ -91,7 +97,7 @@ export interface DecryptedResponse {
|
|||||||
responseData: Uint8Array;
|
responseData: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextPayload {
|
export interface TextPayload extends BasePayload {
|
||||||
type: PayloadType.TEXT;
|
type: PayloadType.TEXT;
|
||||||
dst: NodeHash;
|
dst: NodeHash;
|
||||||
src: NodeHash;
|
src: NodeHash;
|
||||||
@@ -112,12 +118,12 @@ export interface DecryptedText {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AckPayload {
|
export interface AckPayload extends BasePayload {
|
||||||
type: PayloadType.ACK;
|
type: PayloadType.ACK;
|
||||||
checksum: Uint8Array;
|
checksum: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdvertPayload {
|
export interface AdvertPayload extends BasePayload {
|
||||||
type: PayloadType.ADVERT;
|
type: PayloadType.ADVERT;
|
||||||
publicKey: Uint8Array;
|
publicKey: Uint8Array;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
@@ -132,6 +138,13 @@ export enum NodeType {
|
|||||||
SENSOR_NODE = 0x04,
|
SENSOR_NODE = 0x04,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AdvertFlag {
|
||||||
|
HAS_LOCATION = 0x10,
|
||||||
|
HAS_FEATURE1 = 0x20,
|
||||||
|
HAS_FEATURE2 = 0x40,
|
||||||
|
HAS_NAME = 0x80,
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdvertAppData {
|
export interface AdvertAppData {
|
||||||
nodeType: NodeType;
|
nodeType: NodeType;
|
||||||
hasLocation: boolean;
|
hasLocation: boolean;
|
||||||
@@ -144,7 +157,7 @@ export interface AdvertAppData {
|
|||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupTextPayload {
|
export interface GroupTextPayload extends BasePayload {
|
||||||
type: PayloadType.GROUP_TEXT;
|
type: PayloadType.GROUP_TEXT;
|
||||||
channelHash: NodeHash;
|
channelHash: NodeHash;
|
||||||
encrypted: EncryptedPayload;
|
encrypted: EncryptedPayload;
|
||||||
@@ -158,7 +171,7 @@ export interface DecryptedGroupText {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupDataPayload {
|
export interface GroupDataPayload extends BasePayload {
|
||||||
type: PayloadType.GROUP_DATA;
|
type: PayloadType.GROUP_DATA;
|
||||||
channelHash: NodeHash;
|
channelHash: NodeHash;
|
||||||
encrypted: EncryptedPayload;
|
encrypted: EncryptedPayload;
|
||||||
@@ -170,7 +183,7 @@ export interface DecryptedGroupData {
|
|||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnonReqPayload {
|
export interface AnonReqPayload extends BasePayload {
|
||||||
type: PayloadType.ANON_REQ;
|
type: PayloadType.ANON_REQ;
|
||||||
dst: NodeHash;
|
dst: NodeHash;
|
||||||
publicKey: Uint8Array;
|
publicKey: Uint8Array;
|
||||||
@@ -183,13 +196,13 @@ export interface DecryptedAnonReq {
|
|||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PathPayload {
|
export interface PathPayload extends BasePayload {
|
||||||
type: PayloadType.PATH;
|
type: PayloadType.PATH;
|
||||||
dst: NodeHash;
|
dst: NodeHash;
|
||||||
src: NodeHash;
|
src: NodeHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TracePayload {
|
export interface TracePayload extends BasePayload {
|
||||||
type: PayloadType.TRACE;
|
type: PayloadType.TRACE;
|
||||||
tag: number;
|
tag: number;
|
||||||
authCode: number;
|
authCode: number;
|
||||||
@@ -197,7 +210,7 @@ export interface TracePayload {
|
|||||||
nodes: Uint8Array;
|
nodes: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawCustomPayload {
|
export interface RawCustomPayload extends BasePayload {
|
||||||
type: PayloadType.RAW_CUSTOM;
|
type: PayloadType.RAW_CUSTOM;
|
||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/parser.types.ts
Normal file
35
src/parser.types.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import { Packet } from '../src/packet';
|
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';
|
import { hexToBytes, bytesToHex } from '../src/parser';
|
||||||
|
|
||||||
describe('Packet.fromBytes', () => {
|
describe('Packet.fromBytes', () => {
|
||||||
@@ -51,7 +51,7 @@ describe('Packet.fromBytes', () => {
|
|||||||
const pkt = Packet.fromBytes(bytes);
|
const pkt = Packet.fromBytes(bytes);
|
||||||
expect(pkt.routeType).toBe(RouteType.TRANSPORT_DIRECT);
|
expect(pkt.routeType).toBe(RouteType.TRANSPORT_DIRECT);
|
||||||
expect(pkt.payloadType).toBe(PayloadType.TRACE);
|
expect(pkt.payloadType).toBe(PayloadType.TRACE);
|
||||||
const payload = pkt.decode();
|
const payload = pkt.decode() as TracePayload;
|
||||||
expect(payload.type).toBe(PayloadType.TRACE);
|
expect(payload.type).toBe(PayloadType.TRACE);
|
||||||
// the TRACE payload format has been updated; ensure we decode a TRACE payload
|
// the TRACE payload format has been updated; ensure we decode a TRACE payload
|
||||||
expect(payload.type).toBe(PayloadType.TRACE);
|
expect(payload.type).toBe(PayloadType.TRACE);
|
||||||
@@ -148,7 +148,7 @@ describe('Packet decode branches and transport/path parsing', () => {
|
|||||||
|
|
||||||
test('ACK decode and RAW_CUSTOM', () => {
|
test('ACK decode and RAW_CUSTOM', () => {
|
||||||
const ackPayload = new Uint8Array([0x01,0x02,0x03,0x04]);
|
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);
|
expect(ack.type).toBe(PayloadType.ACK);
|
||||||
|
|
||||||
const custom = new Uint8Array([0x99,0x88,0x77]);
|
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', () => {
|
test('GROUP_TEXT and GROUP_DATA decode', () => {
|
||||||
const payload = new Uint8Array([0x55, 0x01, 0x02, 0x03]); // channelHash + mac(2) + cipher
|
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);
|
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);
|
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', () => {
|
test('PATH and TRACE decode nodes', () => {
|
||||||
const pathPayload = new Uint8Array([0x0a, 0x0b]);
|
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);
|
expect(path.type).toBe(PayloadType.PATH);
|
||||||
|
|
||||||
const nodes = new Uint8Array([0x01,0x02,0x03]);
|
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(pathBytes, parts.length);
|
||||||
arr.set(payload, parts.length + pathBytes.length);
|
arr.set(payload, parts.length + pathBytes.length);
|
||||||
const pkt = Packet.fromBytes(arr);
|
const pkt = Packet.fromBytes(arr);
|
||||||
expect(pkt.pathHashCount).toBe(2);
|
expect(pkt.pathHashCount).toBe(3);
|
||||||
expect(pkt.pathHashSize).toBe(3);
|
expect(pkt.pathHashSize).toBe(2);
|
||||||
expect(pkt.pathHashes.length).toBe(2);
|
expect(pkt.pathHashes.length).toBe(3);
|
||||||
expect(pkt.pathHashes[0]).toBe(bytesToHex(pathBytes.subarray(0,3)));
|
expect(pkt.pathHashes[0]).toBe(bytesToHex(pathBytes.subarray(0,2)));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unsupported payload type throws', () => {
|
test('unsupported payload type throws', () => {
|
||||||
@@ -231,3 +231,24 @@ describe('Packet decode branches and transport/path parsing', () => {
|
|||||||
expect(() => pkt.decode()).toThrow();
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user