Reticulum-Go/pkg/interfaces/hdlc_decoder.go
2026-08-13 18:00:59 -05:00

121 lines
3 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package interfaces
// hdlcStreamDecoder incrementally parses HDLC-framed packets from a byte stream.
// Payload bytes are unescaped during assembly. onFrame receives a view of the
// assembler buffer that is reused after the callback returns. Callers that
// retain the frame must copy it. Transport.HandlePacket already copies.
type hdlcStreamDecoder struct {
mtu int
minPayload int
inFrame bool
escape bool
toggle bool
data []byte
maxFrame int
onFrame func([]byte)
}
func newHDLCStreamDecoder(mtu int, onFrame func([]byte)) *hdlcStreamDecoder {
return newHDLCStreamDecoderOpts(mtu, false, 0, onFrame)
}
// newHDLCToggleStreamDecoder uses PPP-style flag toggling, matching TCP read loops.
func newHDLCToggleStreamDecoder(mtu int, onFrame func([]byte)) *hdlcStreamDecoder {
return newHDLCStreamDecoderOpts(mtu, true, 0, onFrame)
}
// newTCPHDLCStreamDecoder matches Python TCPClientInterface.check_frame_len (RNS 1.3.9).
func newTCPHDLCStreamDecoder(mtu int, onFrame func([]byte)) *hdlcStreamDecoder {
return newHDLCStreamDecoderOpts(mtu, true, reticulumHeaderMinSize, onFrame)
}
func newHDLCStreamDecoderOpts(mtu int, toggle bool, minPayload int, onFrame func([]byte)) *hdlcStreamDecoder {
maxFrame := 2*mtu + 32
if maxFrame < 256 {
maxFrame = 2048
}
return &hdlcStreamDecoder{
mtu: mtu,
minPayload: minPayload,
toggle: toggle,
maxFrame: maxFrame,
data: make([]byte, 0, mtu),
onFrame: onFrame,
}
}
func (d *hdlcStreamDecoder) reset() {
d.inFrame = false
d.escape = false
d.data = d.data[:0]
}
func (d *hdlcStreamDecoder) feed(buf []byte) {
for _, b := range buf {
d.feedByte(b)
}
}
// dropPartial clears an incomplete frame. Used when a serial inter-byte idle
// timeout expires so noise does not stick forever in the assembler.
func (d *hdlcStreamDecoder) dropPartial() bool {
if !d.inFrame && len(d.data) == 0 && !d.escape {
return false
}
had := d.inFrame || len(d.data) > 0 || d.escape
d.reset()
if !d.toggle {
d.inFrame = false
}
return had
}
func (d *hdlcStreamDecoder) feedByte(b byte) {
if b == HDLCFlag {
if d.inFrame && len(d.data) > 0 {
// maxFrame allows escaped assembly headroom; delivered payload
// must still fit the interface MTU (matching KISS). TCP/Backbone
// also reject frames at or below HEADER_MINSIZE (RNS 1.3.9).
ok := d.onFrame != nil && (d.mtu <= 0 || len(d.data) <= d.mtu)
if ok && d.minPayload > 0 && len(d.data) <= d.minPayload {
ok = false
}
if ok {
d.onFrame(d.data)
}
}
d.data = d.data[:0]
if d.toggle {
d.inFrame = !d.inFrame
} else {
d.inFrame = true
}
d.escape = false
return
}
if !d.inFrame {
return
}
if b == HDLCEsc {
d.escape = true
return
}
if d.escape {
b ^= HDLCEscMask
d.escape = false
}
limit := d.maxFrame
if d.mtu > 0 && d.mtu < limit {
limit = d.mtu
}
if len(d.data) >= limit {
d.data = d.data[:0]
d.inFrame = false
d.escape = false
return
}
d.data = append(d.data, b)
}