Skip to main content

marlin_binary_transfer/
codec.rs

1//! Layer 1: packet encoding, decoding, and Fletcher-16 checksum.
2//!
3//! This module deals only in bytes and packet structures. It has no notion
4//! of sessions, sync counters, or file transfer.
5//!
6//! # Wire format
7//!
8//! ```text
9//! offset  size  field                     notes
10//! ------  ----  ----------------------    -----------------------------------
11//! 0       2     start_token (0xB5AD)      little-endian; NOT in checksum
12//! 2       1     sync                      wraps mod 256
13//! 3       1     proto<<4 | packet_type    high nibble proto, low nibble type
14//! 4       2     payload_len (u16 LE)
15//! 6       2     header_checksum (u16)     Fletcher-16 of bytes [2..6]
16//! 8       N     payload                   absent if N == 0
17//! 8+N     2     payload_checksum (u16)    Fletcher-16 of bytes [2..8+N]
18//!                                         ABSENT if payload_len == 0
19//! ```
20
21use thiserror::Error;
22
23/// Marlin BFT packet start token. Encoded little-endian on the wire.
24pub const PACKET_TOKEN: u16 = 0xB5AD;
25
26/// Header overhead in bytes: token(2) + sync(1) + proto/type(1) + length(2)
27/// + header_checksum(2).
28pub const HEADER_LEN: usize = 8;
29
30/// Maximum payload size in a single packet (the length field is u16).
31pub const MAX_PAYLOAD: usize = u16::MAX as usize;
32
33/// A protocol packet, borrowed from a buffer.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Packet<'a> {
36    /// Sync counter. Wraps mod 256.
37    pub sync: u8,
38    /// Protocol id. Only the low 4 bits are used; values >15 will fail to encode.
39    pub protocol: u8,
40    /// Packet type within the protocol. Only the low 4 bits are used.
41    pub packet_type: u8,
42    /// Payload bytes. May be empty.
43    pub payload: &'a [u8],
44}
45
46/// Reasons a [`decode`] call may fail.
47#[derive(Debug, Error, PartialEq, Eq)]
48pub enum DecodeError {
49    /// Buffer ended before a complete packet could be read.
50    #[error("not enough bytes (need {need}, have {have})")]
51    Incomplete {
52        /// Number of bytes the decoder needed to be present.
53        need: usize,
54        /// Number of bytes actually available.
55        have: usize,
56    },
57    /// Start token at offset 0 did not match [`PACKET_TOKEN`].
58    #[error("start token mismatch: expected {expected:#06x}, got {got:#06x}")]
59    BadToken {
60        /// The constant token expected.
61        expected: u16,
62        /// The token actually decoded.
63        got: u16,
64    },
65    /// Recomputed Fletcher-16 over the header bytes did not match the stored value.
66    #[error("header checksum mismatch: stored {stored:#06x}, computed {computed:#06x}")]
67    HeaderChecksum {
68        /// Checksum stored in the packet.
69        stored: u16,
70        /// Checksum computed from the header bytes.
71        computed: u16,
72    },
73    /// Recomputed Fletcher-16 over the body bytes did not match the stored value.
74    #[error("payload checksum mismatch: stored {stored:#06x}, computed {computed:#06x}")]
75    PayloadChecksum {
76        /// Checksum stored after the payload.
77        stored: u16,
78        /// Checksum computed from the body bytes.
79        computed: u16,
80    },
81}
82
83/// Reasons an [`encode`] call may fail. None of these can occur for packets
84/// constructed via [`Packet::new`], which validates up front.
85#[derive(Debug, Error, PartialEq, Eq)]
86pub enum EncodeError {
87    /// Payload exceeded [`MAX_PAYLOAD`].
88    #[error("payload length {len} exceeds maximum {MAX_PAYLOAD}")]
89    PayloadTooLarge {
90        /// Length the caller attempted to encode.
91        len: usize,
92    },
93    /// Protocol id was greater than 15.
94    #[error("protocol id {0} out of range (0..=15)")]
95    BadProtocol(u8),
96    /// Packet type was greater than 15.
97    #[error("packet type {0} out of range (0..=15)")]
98    BadPacketType(u8),
99}
100
101impl<'a> Packet<'a> {
102    /// Construct a packet, validating that the protocol id, packet type,
103    /// and payload length all fit on the wire.
104    pub fn new(
105        sync: u8,
106        protocol: u8,
107        packet_type: u8,
108        payload: &'a [u8],
109    ) -> Result<Self, EncodeError> {
110        if protocol > 0xF {
111            return Err(EncodeError::BadProtocol(protocol));
112        }
113        if packet_type > 0xF {
114            return Err(EncodeError::BadPacketType(packet_type));
115        }
116        if payload.len() > MAX_PAYLOAD {
117            return Err(EncodeError::PayloadTooLarge { len: payload.len() });
118        }
119        Ok(Self {
120            sync,
121            protocol,
122            packet_type,
123            payload,
124        })
125    }
126
127    /// Total bytes this packet occupies on the wire.
128    pub fn wire_len(&self) -> usize {
129        if self.payload.is_empty() {
130            HEADER_LEN
131        } else {
132            HEADER_LEN + self.payload.len() + 2
133        }
134    }
135}
136
137/// Fletcher-16, mod-255 variant — exactly matches Marlin's host protocol.
138///
139/// The 16-bit result has the running byte-sum (`sum1`) in its low byte and
140/// the running sum-of-sum1 (`sum2`) in its high byte, both reduced mod 255.
141pub fn fletcher16(buf: &[u8]) -> u16 {
142    let mut cs: u16 = 0;
143    for &b in buf {
144        let cs_low = ((cs & 0xFF) + b as u16) % 255;
145        cs = ((((cs >> 8) + cs_low) % 255) << 8) | cs_low;
146    }
147    cs
148}
149
150/// Encode a packet, appending the wire bytes to `out`.
151///
152/// Returns the number of bytes appended.
153pub fn encode(packet: &Packet<'_>, out: &mut Vec<u8>) -> Result<usize, EncodeError> {
154    if packet.protocol > 0xF {
155        return Err(EncodeError::BadProtocol(packet.protocol));
156    }
157    if packet.packet_type > 0xF {
158        return Err(EncodeError::BadPacketType(packet.packet_type));
159    }
160    if packet.payload.len() > MAX_PAYLOAD {
161        return Err(EncodeError::PayloadTooLarge {
162            len: packet.payload.len(),
163        });
164    }
165
166    let start = out.len();
167
168    // 2-byte start token (NOT in checksum).
169    out.extend_from_slice(&PACKET_TOKEN.to_le_bytes());
170
171    // 6 header bytes covered by the header checksum: sync(1) + proto/type(1)
172    // + payload_len(2) + header_cs(2). We push the first 4 then compute the
173    // checksum over them, then push the checksum.
174    let header_payload_start = out.len();
175    out.push(packet.sync);
176    out.push(((packet.protocol & 0xF) << 4) | (packet.packet_type & 0xF));
177    out.extend_from_slice(&(packet.payload.len() as u16).to_le_bytes());
178    let header_cs = fletcher16(&out[header_payload_start..]);
179    out.extend_from_slice(&header_cs.to_le_bytes());
180
181    if !packet.payload.is_empty() {
182        out.extend_from_slice(packet.payload);
183        let body_end = out.len();
184        // Payload checksum covers everything after the start token: the
185        // 6-byte header (incl. its checksum) plus the payload itself.
186        let payload_cs = fletcher16(&out[header_payload_start..body_end]);
187        out.extend_from_slice(&payload_cs.to_le_bytes());
188    }
189
190    Ok(out.len() - start)
191}
192
193/// Decode a packet from the start of `buf`.
194///
195/// On success, returns the parsed packet and the number of bytes consumed
196/// (i.e. the wire length of the decoded packet). If the buffer is too short
197/// to contain a full packet but is otherwise consistent so far, returns
198/// [`DecodeError::Incomplete`] — callers can read more bytes and retry.
199pub fn decode(buf: &[u8]) -> Result<(Packet<'_>, usize), DecodeError> {
200    if buf.len() < HEADER_LEN {
201        return Err(DecodeError::Incomplete {
202            need: HEADER_LEN,
203            have: buf.len(),
204        });
205    }
206
207    let token = u16::from_le_bytes([buf[0], buf[1]]);
208    if token != PACKET_TOKEN {
209        return Err(DecodeError::BadToken {
210            expected: PACKET_TOKEN,
211            got: token,
212        });
213    }
214
215    let sync = buf[2];
216    let proto_type = buf[3];
217    let protocol = proto_type >> 4;
218    let packet_type = proto_type & 0x0F;
219    let payload_len = u16::from_le_bytes([buf[4], buf[5]]) as usize;
220    let stored_header_cs = u16::from_le_bytes([buf[6], buf[7]]);
221
222    let computed_header_cs = fletcher16(&buf[2..6]);
223    if stored_header_cs != computed_header_cs {
224        return Err(DecodeError::HeaderChecksum {
225            stored: stored_header_cs,
226            computed: computed_header_cs,
227        });
228    }
229
230    if payload_len == 0 {
231        return Ok((
232            Packet {
233                sync,
234                protocol,
235                packet_type,
236                payload: &[],
237            },
238            HEADER_LEN,
239        ));
240    }
241
242    let total_len = HEADER_LEN + payload_len + 2;
243    if buf.len() < total_len {
244        return Err(DecodeError::Incomplete {
245            need: total_len,
246            have: buf.len(),
247        });
248    }
249
250    let payload = &buf[HEADER_LEN..HEADER_LEN + payload_len];
251    let cs_off = HEADER_LEN + payload_len;
252    let stored_payload_cs = u16::from_le_bytes([buf[cs_off], buf[cs_off + 1]]);
253    let computed_payload_cs = fletcher16(&buf[2..cs_off]);
254    if stored_payload_cs != computed_payload_cs {
255        return Err(DecodeError::PayloadChecksum {
256            stored: stored_payload_cs,
257            computed: computed_payload_cs,
258        });
259    }
260
261    Ok((
262        Packet {
263            sync,
264            protocol,
265            packet_type,
266            payload,
267        },
268        total_len,
269    ))
270}