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>
77 lines
2.5 KiB
Go
77 lines
2.5 KiB
Go
// Package packetpath provides shared helpers for extracting path hops from
|
|
// raw MeshCore packet hex bytes.
|
|
package packetpath
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// DecodePathFromRawHex extracts the header path hops directly from raw hex bytes.
|
|
// This is the authoritative path that matches what's in raw_hex, as opposed to
|
|
// decoded.Path.Hops which may be overwritten for TRACE packets (issue #886).
|
|
//
|
|
// WARNING: This function returns the literal header path bytes regardless of
|
|
// payload type. For TRACE packets these bytes are SNR values, NOT hop hashes.
|
|
// Callers that may receive TRACE packets MUST check PathBytesAreHops(payloadType)
|
|
// first, or use the safer DecodeHopsForPayload wrapper.
|
|
func DecodePathFromRawHex(rawHex string) ([]string, error) {
|
|
buf, err := hex.DecodeString(rawHex)
|
|
if err != nil || len(buf) < 2 {
|
|
return nil, fmt.Errorf("invalid or too-short hex")
|
|
}
|
|
|
|
headerByte := buf[0]
|
|
offset := 1
|
|
if IsTransportRoute(int(headerByte & 0x03)) {
|
|
if len(buf) < offset+4 {
|
|
return nil, fmt.Errorf("too short for transport codes")
|
|
}
|
|
offset += 4
|
|
}
|
|
if offset >= len(buf) {
|
|
return nil, fmt.Errorf("too short for path byte")
|
|
}
|
|
|
|
pathByte := buf[offset]
|
|
offset++
|
|
|
|
hashSize := int(pathByte>>6) + 1
|
|
hashCount := int(pathByte & 0x3F)
|
|
|
|
hops := make([]string, 0, hashCount)
|
|
for i := 0; i < hashCount; i++ {
|
|
start := offset + i*hashSize
|
|
end := start + hashSize
|
|
if end > len(buf) {
|
|
break
|
|
}
|
|
hops = append(hops, strings.ToUpper(hex.EncodeToString(buf[start:end])))
|
|
}
|
|
return hops, nil
|
|
}
|
|
|
|
// DecodeHopsForPayload returns the header path hops only when the payload type's
|
|
// header bytes are actually route hops (i.e. PathBytesAreHops(payloadType) is true).
|
|
// For TRACE packets it returns (nil, ErrPayloadHasNoHeaderHops) so the caller is
|
|
// forced to source hops from the decoded payload instead.
|
|
//
|
|
// Prefer this over DecodePathFromRawHex when the payload type is known.
|
|
func DecodeHopsForPayload(rawHex string, payloadType byte) ([]string, error) {
|
|
if !PathBytesAreHops(payloadType) {
|
|
return nil, ErrPayloadHasNoHeaderHops
|
|
}
|
|
return DecodePathFromRawHex(rawHex)
|
|
}
|
|
|
|
// ErrPayloadHasNoHeaderHops is returned by DecodeHopsForPayload when the
|
|
// payload type repurposes the raw_hex header path bytes (e.g. TRACE → SNR values).
|
|
var ErrPayloadHasNoHeaderHops = errPayloadHasNoHeaderHops{}
|
|
|
|
type errPayloadHasNoHeaderHops struct{}
|
|
|
|
func (errPayloadHasNoHeaderHops) Error() string {
|
|
return "payload type repurposes header path bytes; source hops from decoded payload"
|
|
}
|