Files
meshcore-analyzer/internal/packetpath/path.go
T
Sylvain Rabot b5b230e884 feat(channels): show the sender's path hash size on each message (#2089)
Each channel message now shows the hash size its sender's path uses, read
from bits 7-6 of the path byte that the originator writes and repeaters keep.
The server sends it as path_hash_size (packetpath.HashSize); the frontend
helper pathHashSize() applies the same rule and returns 0 for unknown.

One rule on both pages: the packet detail Hash Size row and the hex
breakdown call pathHashSize() too, so a 0-hop flood message reports the
same size on Channels and on its packet page. A direct packet with no hops
left reports no size, matching cmd/server/decoder.go. The cases live in
test-fixtures/path-hash-size-cases.json, read by the Go and JS tests.
2026-10-05 20:32:40 +02:00

123 lines
3.9 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
}
// HashSize returns the path hash size (1-3 bytes) the originator chose, read
// from the path byte of raw_hex, or 0 when the packet does not carry one.
//
// A flood packet always carries it: Mesh::sendFlood sets the path byte to
// (size-1)<<6 before the first hop, so it holds even at 0 hops. A direct
// packet with no hops carries none: Mesh::sendZeroHop writes 0x00, and a
// direct path that has run out of hops has no hash left to size. That is the
// same rule cmd/server/decoder.go applies to path.hashSize. TRACE path bytes
// are SNR readings, not hashes. Only the first few bytes are parsed, so this
// is cheap on hot paths.
func HashSize(rawHex string) int {
header, ok := hexByteAt(rawHex, 0)
if !ok || !PathBytesAreHops(header>>2&0x0F) {
return 0
}
routeType := int(header & 0x03)
offset := 1
if IsTransportRoute(routeType) {
offset += 4
}
pathByte, ok := hexByteAt(rawHex, offset)
if !ok {
return 0
}
if pathByte&0x3F == 0 && (routeType == RouteDirect || routeType == RouteTransportDirect) {
return 0
}
size := int(pathByte>>6) + 1
if size > 3 {
return 0 // 0b11 is reserved; firmware rejects sizes above 3
}
return size
}
// hexByteAt decodes the byte at byte index i of a hex string.
func hexByteAt(s string, i int) (byte, bool) {
if len(s) < 2*i+2 {
return 0, false
}
b, err := hex.DecodeString(s[2*i : 2*i+2])
if err != nil {
return 0, false
}
return b[0], true
}
// 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"
}