diff --git a/README.md b/README.md index c7c37a5..0720087 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,16 @@ The `analyzeStructure()` method provides: - **Payload field parsing** for all supported packet types - **Human-readable descriptions** for each field +MeshCore's packet `path_len` byte is a packed field, not a raw byte count: +- low 6 bits = number of path hashes +- high 2 bits = hash width minus 1 + +The decoder exposes: +- `pathLength`: raw encoded `path_len` byte +- `pathHashCount`: decoded hop-hash count +- `pathHashSize`: decoded hop-hash width in bytes +- `pathByteLength`: actual path byte length on the wire + ## Ed25519 Key Derivation The library includes MeshCore-compatible Ed25519 key derivation using the exact orlp/ed25519 algorithm via WebAssembly: @@ -450,4 +460,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/src/decoder/packet-decoder.ts b/src/decoder/packet-decoder.ts index a2cfff0..58c0662 100644 --- a/src/decoder/packet-decoder.ts +++ b/src/decoder/packet-decoder.ts @@ -68,6 +68,9 @@ export class MeshCorePacketDecoder { payloadType: PayloadType.RawCustom, payloadVersion: PayloadVersion.Version1, pathLength: 0, + pathHashCount: 0, + pathHashSize: 0, + pathByteLength: 0, path: null, payload: { raw: '', decoded: null }, totalBytes: bytes.length, @@ -162,13 +165,19 @@ export class MeshCorePacketDecoder { throw new Error('Packet too short for path length'); } const pathLength = bytes[offset]; + const pathHashCount = pathLength & 0x3F; + const pathHashSize = (pathLength >> 6) + 1; + if (pathHashSize === 4) { + throw new Error('Invalid path encoding: 4-byte path hashes are reserved'); + } + const pathByteLength = pathHashCount * pathHashSize; if (includeStructure) { - let pathLengthDescription = `Path contains ${pathLength} bytes`; + let pathLengthDescription = `Packed path encoding: ${pathHashCount} hop hashes x ${pathHashSize} byte(s) = ${pathByteLength} path bytes`; if (routeType === RouteType.Direct || routeType === RouteType.TransportDirect) { - pathLengthDescription = `For "Direct" packets, this contains routing instructions. ${pathLength} bytes of routing instructions (decreases as packet travels)`; + pathLengthDescription = `Packed direct-path encoding: ${pathHashCount} hop hashes x ${pathHashSize} byte(s) = ${pathByteLength} routing bytes`; } else if (routeType === RouteType.Flood || routeType === RouteType.TransportFlood) { - pathLengthDescription = `${pathLength} bytes showing route taken (increases as packet floods)`; + pathLengthDescription = `Packed flood-path encoding: ${pathHashCount} hop hashes x ${pathHashSize} byte(s) = ${pathByteLength} route bytes`; } segments.push({ @@ -181,19 +190,19 @@ export class MeshCorePacketDecoder { } offset += 1; - if (bytes.length < offset + pathLength) { + if (bytes.length < offset + pathByteLength) { throw new Error('Packet too short for path data'); } // convert path data to hex strings - const pathBytes = bytes.subarray(offset, offset + pathLength); - const path: string[] | null = pathLength > 0 ? Array.from(pathBytes).map(byteToHex) : null; + const pathBytes = bytes.subarray(offset, offset + pathByteLength); + const path: string[] | null = pathByteLength > 0 ? Array.from(pathBytes).map(byteToHex) : null; - if (includeStructure && pathLength > 0) { + if (includeStructure && pathByteLength > 0) { if (payloadType === PayloadType.Trace) { // TRACE packets have SNR values in path const snrValues = []; - for (let i = 0; i < pathLength; i++) { + for (let i = 0; i < pathByteLength; i++) { const snrRaw = bytes[offset + i]; const snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw; const snrDb = snrSigned / 4.0; @@ -203,8 +212,8 @@ export class MeshCorePacketDecoder { name: 'Path SNR Data', description: `SNR values collected during trace: ${snrValues.join(', ')}`, startByte: offset, - endByte: offset + pathLength - 1, - value: bytesToHex(bytes.slice(offset, offset + pathLength)) + endByte: offset + pathByteLength - 1, + value: bytesToHex(bytes.slice(offset, offset + pathByteLength)) }); } else { let pathDescription = 'Routing path information'; @@ -218,12 +227,12 @@ export class MeshCorePacketDecoder { name: 'Path Data', description: pathDescription, startByte: offset, - endByte: offset + pathLength - 1, - value: bytesToHex(bytes.slice(offset, offset + pathLength)) + endByte: offset + pathByteLength - 1, + value: bytesToHex(bytes.slice(offset, offset + pathByteLength)) }); } } - offset += pathLength; + offset += pathByteLength; // extract payload const payloadBytes = bytes.subarray(offset); @@ -359,6 +368,9 @@ export class MeshCorePacketDecoder { payloadVersion, transportCodes, pathLength, + pathHashCount, + pathHashSize, + pathByteLength, path, payload: { raw: payloadHex, @@ -390,6 +402,9 @@ export class MeshCorePacketDecoder { payloadType: PayloadType.RawCustom, payloadVersion: PayloadVersion.Version1, pathLength: 0, + pathHashCount: 0, + pathHashSize: 0, + pathByteLength: 0, path: null, payload: { raw: '', decoded: null }, totalBytes: bytes.length, diff --git a/src/types/packet.ts b/src/types/packet.ts index 1265b94..c119c41 100644 --- a/src/types/packet.ts +++ b/src/types/packet.ts @@ -14,6 +14,9 @@ export interface DecodedPacket { // transport and routing transportCodes?: [number, number]; pathLength: number; + pathHashCount: number; + pathHashSize: number; + pathByteLength: number; path: string[] | null; // payload data diff --git a/tests/ack-path.test.ts b/tests/ack-path.test.ts index ebead65..48c2967 100644 --- a/tests/ack-path.test.ts +++ b/tests/ack-path.test.ts @@ -14,6 +14,9 @@ describe('Ack/Path Packet Decoding', () => { expect(result.isValid).toBe(true); expect(result.payloadType).toBe(PayloadType.Ack); expect(result.pathLength).toBe(4); + expect(result.pathHashCount).toBe(4); + expect(result.pathHashSize).toBe(1); + expect(result.pathByteLength).toBe(4); if (result.payload.decoded && 'type' in result.payload.decoded && result.payload.decoded.type === PayloadType.Ack) { const ackPayload = result.payload.decoded as AckPayload; @@ -35,6 +38,9 @@ describe('Ack/Path Packet Decoding', () => { expect(result.isValid).toBe(true); expect(result.payloadType).toBe(PayloadType.Path); expect(result.pathLength).toBe(5); // 5 bytes in packet-level path + expect(result.pathHashCount).toBe(5); + expect(result.pathHashSize).toBe(1); + expect(result.pathByteLength).toBe(5); expect(result.path).toEqual(['F4', '64', 'C7', '7E', '41']); // Packet-level path data expect(result.messageHash).toBe('A574CE1D'); expect(result.totalBytes).toBe(27); diff --git a/tests/packet-structure.test.ts b/tests/packet-structure.test.ts index a3159b8..5ce2359 100644 --- a/tests/packet-structure.test.ts +++ b/tests/packet-structure.test.ts @@ -248,7 +248,8 @@ describe('Packet Structure Analysis', () => { const pathDataSegment = structure.segments.find(s => s.name === 'Path Data'); expect(pathLengthSegment).toBeDefined(); - expect(pathLengthSegment!.value).toBe('0x04'); // 4 bytes of path data + expect(pathLengthSegment!.value).toBe('0x04'); + expect(pathLengthSegment!.description).toContain('4 hop hashes x 1 byte(s) = 4 route bytes'); expect(pathDataSegment).toBeDefined(); expect(pathDataSegment!.value).toBe('6F17C47E'); // Path bytes