From 8f833f64ae2608b032a37d7d1805ee91badfa5f7 Mon Sep 17 00:00:00 2001 From: efiten Date: Mon, 30 Mar 2026 18:27:50 +0200 Subject: [PATCH] fix: parse TRACE packet path hops from payload instead of header (#277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #276 ## Root cause TRACE packets store hop IDs in the payload (bytes 9+) rather than in the header path field. The header path field is overloaded in TRACE packets to carry RSSI values instead of repeater IDs (as noted in the issue comments). This meant `Path.Hops` was always empty for TRACE packets — the raw bytes ended up as an opaque `PathData` hex string with no structure. The hashSize encoded in the header path byte (bits 6–7) is still valid for TRACE and is used to split the payload path bytes into individual hop prefixes. ## Fix After decoding a TRACE payload, if `PathData` is non-empty, parse it into individual hops using `path.HashSize`: ```go if header.PayloadType == PayloadTRACE && payload.PathData != "" { pathBytes, err := hex.DecodeString(payload.PathData) if err == nil && path.HashSize > 0 { for i := 0; i+path.HashSize <= len(pathBytes); i += path.HashSize { path.Hops = append(path.Hops, ...) } } } ``` Applied to both `cmd/ingestor/decoder.go` and `cmd/server/decoder.go`. ## Verification Packet from the issue: `260001807dca00000000007d547d` | | Before | After | |---|---|---| | `Path.Hops` | `[]` | `["7D", "54", "7D"]` | | `Path.HashCount` | `0` | `3` | New test `TestDecodeTracePathParsing` covers this exact packet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 --- cmd/ingestor/decoder.go | 15 +++++++++++++++ cmd/ingestor/decoder_test.go | 25 +++++++++++++++++++++++++ cmd/server/decoder.go | 15 +++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/cmd/ingestor/decoder.go b/cmd/ingestor/decoder.go index 708d5b40..78cc4f71 100644 --- a/cmd/ingestor/decoder.go +++ b/cmd/ingestor/decoder.go @@ -572,6 +572,21 @@ func DecodePacket(hexString string, channelKeys map[string]string) (*DecodedPack payloadBuf := buf[offset:] payload := decodePayload(header.PayloadType, payloadBuf, channelKeys) + // TRACE packets store hop IDs in the payload (buf[9:]) rather than the header + // path field. The header path byte still encodes hashSize in bits 6-7, which + // we use to split the payload path data into individual hop prefixes. + if header.PayloadType == PayloadTRACE && payload.PathData != "" { + pathBytes, err := hex.DecodeString(payload.PathData) + if err == nil && path.HashSize > 0 { + hops := make([]string, 0, len(pathBytes)/path.HashSize) + for i := 0; i+path.HashSize <= len(pathBytes); i += path.HashSize { + hops = append(hops, strings.ToUpper(hex.EncodeToString(pathBytes[i:i+path.HashSize]))) + } + path.Hops = hops + path.HashCount = len(hops) + } + } + return &DecodedPacket{ Header: header, TransportCodes: tc, diff --git a/cmd/ingestor/decoder_test.go b/cmd/ingestor/decoder_test.go index 51ae9896..1b60bc94 100644 --- a/cmd/ingestor/decoder_test.go +++ b/cmd/ingestor/decoder_test.go @@ -564,6 +564,31 @@ func TestDecodeTraceValid(t *testing.T) { } } +func TestDecodeTracePathParsing(t *testing.T) { + // Packet from issue #276: 260001807dca00000000007d547d + // Path byte 0x00 → hashSize=1, hops in payload at buf[9:] = 7d 54 7d + // Expected path: ["7D", "54", "7D"] + pkt, err := DecodePacket("260001807dca00000000007d547d", nil) + if err != nil { + t.Fatalf("DecodePacket error: %v", err) + } + if pkt.Payload.Type != "TRACE" { + t.Errorf("payload type=%s, want TRACE", pkt.Payload.Type) + } + want := []string{"7D", "54", "7D"} + if len(pkt.Path.Hops) != len(want) { + t.Fatalf("hops=%v, want %v", pkt.Path.Hops, want) + } + for i, h := range want { + if pkt.Path.Hops[i] != h { + t.Errorf("hops[%d]=%s, want %s", i, pkt.Path.Hops[i], h) + } + } + if pkt.Path.HashCount != 3 { + t.Errorf("hashCount=%d, want 3", pkt.Path.HashCount) + } +} + func TestDecodeAdvertShort(t *testing.T) { p := decodeAdvert(make([]byte, 50)) if p.Error != "too short for advert" { diff --git a/cmd/server/decoder.go b/cmd/server/decoder.go index 8058e022..c67fa9e2 100644 --- a/cmd/server/decoder.go +++ b/cmd/server/decoder.go @@ -373,6 +373,21 @@ func DecodePacket(hexString string) (*DecodedPacket, error) { payloadBuf := buf[offset:] payload := decodePayload(header.PayloadType, payloadBuf) + // TRACE packets store hop IDs in the payload (buf[9:]) rather than the header + // path field. The header path byte still encodes hashSize in bits 6-7, which + // we use to split the payload path data into individual hop prefixes. + if header.PayloadType == PayloadTRACE && payload.PathData != "" { + pathBytes, err := hex.DecodeString(payload.PathData) + if err == nil && path.HashSize > 0 { + hops := make([]string, 0, len(pathBytes)/path.HashSize) + for i := 0; i+path.HashSize <= len(pathBytes); i += path.HashSize { + hops = append(hops, strings.ToUpper(hex.EncodeToString(pathBytes[i:i+path.HashSize]))) + } + path.Hops = hops + path.HashCount = len(hops) + } + } + return &DecodedPacket{ Header: header, TransportCodes: tc,