mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-26 01:02:31 +00:00
## Problem Per-observation `path_json` disagrees with `raw_hex` path section for TRACE packets. **Reproducer:** packet `af081a2c41281b1e`, observer `lutin🏡` - `path_json`: `["67","33","D6","33","67"]` (5 hops — from TRACE payload) - `raw_hex` path section: `30 2D 0D 23` (4 bytes — SNR values in header) ## Root Cause `DecodePacket` correctly parses TRACE packets by replacing `path.Hops` with hop IDs from the payload's `pathData` field (the actual route). However, the header path bytes for TRACE packets contain **SNR values** (one per completed hop), not hop IDs. `BuildPacketData` used `decoded.Path.Hops` to build `path_json`, which for TRACE packets contained the payload-derived hops — not the header path bytes that `raw_hex` stores. This caused `path_json` and `raw_hex` to describe completely different paths. ## Fix - Added `DecodePathFromRawHex(rawHex)` — extracts header path hops directly from raw hex bytes, independent of any TRACE payload overwriting. - `BuildPacketData` now calls `DecodePathFromRawHex(msg.Raw)` instead of using `decoded.Path.Hops`, guaranteeing `path_json` always matches the `raw_hex` path section. ## Tests (8 new) **`DecodePathFromRawHex` unit tests:** - hash_size 1, 2, 3, 4 - zero-hop direct packets - transport route (4-byte transport codes before path) **`BuildPacketData` integration tests:** - TRACE packet: asserts path_json matches raw_hex header path (not payload hops) - Non-TRACE packet: asserts path_json matches raw_hex header path All existing tests continue to pass (`go test ./...` for both ingestor and server). Fixes #886 --------- Co-authored-by: you <you@example.com>
151 lines
4.9 KiB
Go
151 lines
4.9 KiB
Go
package packetpath
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDecodePathFromRawHex_Basic(t *testing.T) {
|
|
// Build a simple FLOOD packet (route_type=1) with 2 hops of hashSize=1
|
|
// header: route_type=1, payload_type=2 (TXT_MSG), version=0 → 0b00_0010_01 = 0x09
|
|
// path byte: hashSize=1 (bits 7-6 = 0), hashCount=2 (bits 5-0 = 2) → 0x02
|
|
// hops: AB, CD
|
|
// payload: some bytes
|
|
raw := "0902ABCD" + "DEADBEEF"
|
|
hops, err := DecodePathFromRawHex(raw)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(hops) != 2 || hops[0] != "AB" || hops[1] != "CD" {
|
|
t.Fatalf("expected [AB, CD], got %v", hops)
|
|
}
|
|
}
|
|
|
|
func TestDecodePathFromRawHex_ZeroHops(t *testing.T) {
|
|
// DIRECT route (type=2), no hops → 0b00_0010_10 = 0x0A
|
|
// path byte: 0x00 (0 hops)
|
|
raw := "0A00" + "DEADBEEF"
|
|
hops, err := DecodePathFromRawHex(raw)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(hops) != 0 {
|
|
t.Fatalf("expected 0 hops, got %v", hops)
|
|
}
|
|
}
|
|
|
|
func TestDecodePathFromRawHex_TransportRoute(t *testing.T) {
|
|
// TRANSPORT_FLOOD (route_type=0), payload_type=5 (GRP_TXT), version=0
|
|
// header: 0b00_0101_00 = 0x14
|
|
// transport codes: 4 bytes
|
|
// path byte: hashSize=1, hashCount=1 → 0x01
|
|
// hop: FF
|
|
raw := "14" + "00112233" + "01" + "FF" + "DEAD"
|
|
hops, err := DecodePathFromRawHex(raw)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(hops) != 1 || hops[0] != "FF" {
|
|
t.Fatalf("expected [FF], got %v", hops)
|
|
}
|
|
}
|
|
|
|
// buildTracePacket creates a TRACE packet hex string where header path bytes are
|
|
// SNR values, and payload contains the actual route hops.
|
|
func buildTracePacket() (rawHex string, headerPathHops []string, payloadHops []string) {
|
|
// DIRECT route (type=2), TRACE payload (type=9), version=0
|
|
// header byte: 0b00_1001_10 = 0x26
|
|
headerByte := byte(0x26)
|
|
|
|
// Header path: 2 SNR bytes (hashSize=1, hashCount=2) → path byte = 0x02
|
|
// SNR values: 0x1A (26 dB), 0x0F (15 dB)
|
|
pathByte := byte(0x02)
|
|
snrBytes := []byte{0x1A, 0x0F}
|
|
|
|
// TRACE payload: tag(4) + authCode(4) + flags(1) + path hops
|
|
tag := []byte{0x01, 0x00, 0x00, 0x00}
|
|
authCode := []byte{0x02, 0x00, 0x00, 0x00}
|
|
// flags: path_sz=0 (1 byte hops), other bits=0 → 0x00
|
|
flags := byte(0x00)
|
|
// Payload hops: AA, BB, CC (the actual route)
|
|
payloadPathBytes := []byte{0xAA, 0xBB, 0xCC}
|
|
|
|
var buf []byte
|
|
buf = append(buf, headerByte, pathByte)
|
|
buf = append(buf, snrBytes...)
|
|
buf = append(buf, tag...)
|
|
buf = append(buf, authCode...)
|
|
buf = append(buf, flags)
|
|
buf = append(buf, payloadPathBytes...)
|
|
|
|
rawHex = strings.ToUpper(hex.EncodeToString(buf))
|
|
headerPathHops = []string{"1A", "0F"} // SNR values — NOT route hops
|
|
payloadHops = []string{"AA", "BB", "CC"} // actual route hops from payload
|
|
return
|
|
}
|
|
|
|
func TestDecodePathFromRawHex_TraceReturnsSNR(t *testing.T) {
|
|
rawHex, expectedSNR, _ := buildTracePacket()
|
|
hops, err := DecodePathFromRawHex(rawHex)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
// DecodePathFromRawHex always returns header path bytes — for TRACE these are SNR values
|
|
if len(hops) != len(expectedSNR) {
|
|
t.Fatalf("expected %d hops (SNR), got %d: %v", len(expectedSNR), len(hops), hops)
|
|
}
|
|
for i, h := range hops {
|
|
if h != expectedSNR[i] {
|
|
t.Errorf("hop[%d]: expected %s, got %s", i, expectedSNR[i], h)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTracePathJSON_UsesPayloadHops(t *testing.T) {
|
|
// This test validates the TRACE vs non-TRACE logic that callers should implement:
|
|
// For TRACE: path_json = decoded.Path.Hops (payload-decoded route hops)
|
|
// For non-TRACE: path_json = DecodePathFromRawHex(raw_hex)
|
|
rawHex, snrHops, payloadHops := buildTracePacket()
|
|
|
|
// DecodePathFromRawHex returns SNR bytes for TRACE
|
|
headerHops, _ := DecodePathFromRawHex(rawHex)
|
|
headerJSON, _ := json.Marshal(headerHops)
|
|
|
|
// payload hops (what decoded.Path.Hops would return after TRACE decoding)
|
|
payloadJSON, _ := json.Marshal(payloadHops)
|
|
|
|
// They must differ — SNR != route hops
|
|
if string(headerJSON) == string(payloadJSON) {
|
|
t.Fatalf("SNR hops and payload hops should differ for TRACE; both are %s", headerJSON)
|
|
}
|
|
|
|
// For TRACE, path_json should be payloadHops, not headerHops
|
|
_ = snrHops // snrHops == headerHops — used for documentation
|
|
t.Logf("TRACE: header path (SNR) = %s, payload path (route) = %s", headerJSON, payloadJSON)
|
|
}
|
|
|
|
func TestDecodeHopsForPayload_NonTrace(t *testing.T) {
|
|
// header 0x01, path_len 0x02, hops 0xAA 0xBB, then payload bytes
|
|
raw := "0102AABB00"
|
|
hops, err := DecodeHopsForPayload(raw, 0x05) // GRP_TXT — header path bytes ARE hops
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(hops) != 2 || hops[0] != "AA" || hops[1] != "BB" {
|
|
t.Errorf("expected [AA BB], got %v", hops)
|
|
}
|
|
}
|
|
|
|
func TestDecodeHopsForPayload_TraceReturnsError(t *testing.T) {
|
|
raw := "010205F00100"
|
|
hops, err := DecodeHopsForPayload(raw, PayloadTRACE)
|
|
if err != ErrPayloadHasNoHeaderHops {
|
|
t.Errorf("expected ErrPayloadHasNoHeaderHops, got %v", err)
|
|
}
|
|
if hops != nil {
|
|
t.Errorf("expected nil hops for TRACE, got %v", hops)
|
|
}
|
|
}
|