Skip to main content

donglora_client/
codec.rs

1//! COBS framing: encode/decode frames and accumulate partial reads.
2//!
3//! Each frame is COBS-encoded and terminated with a `0x00` sentinel byte.
4//! The sentinel never appears in the encoded data, so it unambiguously
5//! marks frame boundaries.
6//!
7//! Uses the [`ucobs`] crate — the same COBS implementation as the firmware.
8
9use tracing::warn;
10
11/// COBS-encode data and append the `0x00` sentinel.
12pub fn encode_frame(data: &[u8]) -> Vec<u8> {
13    let max_encoded = ucobs::max_encoded_len(data.len());
14    let mut buf = vec![0u8; max_encoded];
15    let n = match ucobs::encode(data, &mut buf) {
16        Some(n) => n,
17        None => return vec![0x00], // empty/failed encode → just sentinel
18    };
19    buf.truncate(n);
20    buf.push(0x00);
21    buf
22}
23
24/// COBS-decode a frame (without the sentinel). Returns `None` on decode error.
25pub fn decode_frame(encoded: &[u8]) -> Option<Vec<u8>> {
26    if encoded.is_empty() {
27        return None;
28    }
29    let mut buf = vec![0u8; encoded.len()];
30    let n = ucobs::decode(encoded, &mut buf)?;
31    buf.truncate(n);
32    Some(buf)
33}
34
35/// Stateful COBS frame accumulator.
36///
37/// Feed raw byte chunks (from serial reads, socket reads, etc.) and extract
38/// complete decoded frames. Handles partial reads, multiple frames per chunk,
39/// and empty inter-frame gaps gracefully.
40///
41/// Used by both the sync client (`read_frame`) and the async mux to accumulate
42/// data from serial and client socket streams.
43pub struct FrameReader {
44    buf: Vec<u8>,
45}
46
47impl FrameReader {
48    /// Create a new empty frame reader.
49    pub fn new() -> Self {
50        Self { buf: Vec::with_capacity(512) }
51    }
52
53    /// Feed raw bytes and return all complete decoded frames.
54    ///
55    /// COBS decode errors are logged and the bad frame is skipped.
56    /// Empty inter-frame gaps (consecutive `0x00` bytes) are ignored.
57    pub fn feed(&mut self, data: &[u8]) -> Vec<Vec<u8>> {
58        self.buf.extend_from_slice(data);
59
60        let mut frames = Vec::new();
61        while let Some(sentinel_pos) = self.buf.iter().position(|&b| b == 0x00) {
62            let encoded = &self.buf[..sentinel_pos];
63            if !encoded.is_empty() {
64                match decode_frame(encoded) {
65                    Some(decoded) => frames.push(decoded),
66                    None => warn!("bad COBS frame ({} bytes) — skipped", encoded.len()),
67                }
68            }
69            // Remove the consumed bytes including the sentinel
70            self.buf.drain(..=sentinel_pos);
71        }
72        frames
73    }
74
75    /// Return the number of buffered bytes not yet forming a complete frame.
76    pub fn buffered(&self) -> usize {
77        self.buf.len()
78    }
79}
80
81impl Default for FrameReader {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87/// Read one complete COBS frame from a blocking `Read` source.
88///
89/// Reads byte-by-byte until a `0x00` sentinel is found, then COBS-decodes.
90/// Returns `Ok(None)` on timeout (zero-length read from the underlying source).
91/// Returns `Err` on I/O errors or COBS decode failures.
92pub fn read_frame(reader: &mut dyn std::io::Read) -> anyhow::Result<Option<Vec<u8>>> {
93    let mut buf = Vec::with_capacity(280);
94    let mut byte = [0u8; 1];
95
96    loop {
97        match reader.read(&mut byte) {
98            Ok(0) => {
99                // Timeout or EOF
100                return Ok(None);
101            }
102            Ok(_) => {
103                if byte[0] == 0x00 {
104                    break;
105                }
106                buf.push(byte[0]);
107            }
108            Err(e) if matches!(e.kind(), std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock) => {
109                return Ok(None);
110            }
111            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
112            Err(e) => return Err(e.into()),
113        }
114    }
115
116    if buf.is_empty() {
117        return Ok(None);
118    }
119
120    decode_frame(&buf).map(Some).ok_or_else(|| anyhow::anyhow!("COBS decode error"))
121}
122
123// ── Tests ──────────────────────────────────────────────────────────
124
125#[cfg(test)]
126#[allow(clippy::unwrap_used, clippy::expect_used)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn encode_decode_roundtrip() {
132        let data = b"hello world";
133        let encoded = encode_frame(data);
134        // Encoded should end with 0x00 sentinel
135        assert_eq!(encoded.last(), Some(&0x00));
136        // Encoded should not contain 0x00 except the trailing sentinel
137        assert!(!encoded[..encoded.len() - 1].contains(&0x00));
138        // Decode (without sentinel)
139        let decoded = decode_frame(&encoded[..encoded.len() - 1]);
140        assert_eq!(decoded.as_deref(), Some(data.as_slice()));
141    }
142
143    #[test]
144    fn encode_decode_empty() {
145        // COBS encoding of empty data produces just the 0x00 sentinel.
146        // This is equivalent to an empty inter-frame gap and is correctly
147        // skipped by both FrameReader and read_frame.
148        let encoded = encode_frame(b"");
149        assert_eq!(encoded.last(), Some(&0x00));
150    }
151
152    #[test]
153    fn encode_decode_with_zeros() {
154        let data = &[0x00, 0x01, 0x00, 0x02, 0x00];
155        let encoded = encode_frame(data);
156        let decoded = decode_frame(&encoded[..encoded.len() - 1]);
157        assert_eq!(decoded.as_deref(), Some(data.as_slice()));
158    }
159
160    #[test]
161    fn decode_frame_invalid() {
162        assert!(decode_frame(&[]).is_none());
163    }
164
165    #[test]
166    fn frame_reader_single_frame() {
167        let mut reader = FrameReader::new();
168        let data = b"test";
169        let frame = encode_frame(data);
170        let frames = reader.feed(&frame);
171        assert_eq!(frames.len(), 1);
172        assert_eq!(frames[0], data);
173    }
174
175    #[test]
176    fn frame_reader_multiple_frames_at_once() {
177        let mut reader = FrameReader::new();
178        let f1 = encode_frame(b"one");
179        let f2 = encode_frame(b"two");
180        let mut combined = f1;
181        combined.extend_from_slice(&f2);
182        let frames = reader.feed(&combined);
183        assert_eq!(frames.len(), 2);
184        assert_eq!(frames[0], b"one");
185        assert_eq!(frames[1], b"two");
186    }
187
188    #[test]
189    fn frame_reader_partial_then_complete() {
190        let mut reader = FrameReader::new();
191        let frame = encode_frame(b"split");
192        let mid = frame.len() / 2;
193
194        // Feed first half — no complete frames yet
195        let frames = reader.feed(&frame[..mid]);
196        assert!(frames.is_empty());
197        assert!(reader.buffered() > 0);
198
199        // Feed second half — now we get the frame
200        let frames = reader.feed(&frame[mid..]);
201        assert_eq!(frames.len(), 1);
202        assert_eq!(frames[0], b"split");
203        assert_eq!(reader.buffered(), 0);
204    }
205
206    #[test]
207    fn frame_reader_empty_gaps() {
208        let mut reader = FrameReader::new();
209        // Multiple sentinels in a row (empty gaps between frames)
210        let f = encode_frame(b"data");
211        let mut input = vec![0x00, 0x00]; // leading empty gaps
212        input.extend_from_slice(&f);
213        input.push(0x00); // trailing empty gap
214        let frames = reader.feed(&input);
215        assert_eq!(frames.len(), 1);
216        assert_eq!(frames[0], b"data");
217    }
218
219    #[test]
220    fn frame_reader_bad_cobs_skipped() {
221        let mut reader = FrameReader::new();
222        // Feed a known-bad COBS frame followed by a good one.
223        // A frame where the first byte claims a run longer than the frame is invalid.
224        let bad = &[0xFF, 0x01, 0x00]; // 0xFF says next 254 bytes, but only 1 present
225        let good = encode_frame(b"ok");
226        let mut input = bad.to_vec();
227        input.extend_from_slice(&good);
228        let frames = reader.feed(&input);
229        // Bad frame skipped, good frame decoded
230        assert_eq!(frames.len(), 1);
231        assert_eq!(frames[0], b"ok");
232    }
233
234    #[test]
235    fn read_frame_from_bytes() {
236        let data = b"frame";
237        let encoded = encode_frame(data);
238        let mut cursor = std::io::Cursor::new(encoded);
239        let result = read_frame(&mut cursor);
240        assert!(result.is_ok());
241        assert_eq!(result.ok().flatten().as_deref(), Some(data.as_slice()));
242    }
243
244    #[test]
245    fn read_frame_timeout() {
246        // Empty reader → returns None (timeout)
247        let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
248        let result = read_frame(&mut cursor);
249        assert!(result.is_ok());
250        assert!(result.ok().flatten().is_none());
251    }
252
253    #[test]
254    fn read_frame_empty_gap_then_data() {
255        let data = b"after_gap";
256        let mut input = vec![0x00]; // empty gap
257        input.extend_from_slice(&encode_frame(data));
258        let mut cursor = std::io::Cursor::new(input);
259        // First read_frame returns None for the empty gap
260        let r1 = read_frame(&mut cursor);
261        assert_eq!(r1.ok().flatten(), None);
262        // Second read returns the actual frame
263        let r2 = read_frame(&mut cursor);
264        assert_eq!(r2.ok().flatten().as_deref(), Some(data.as_slice()));
265    }
266
267    #[test]
268    fn encode_all_byte_values_roundtrip() {
269        // Test that all possible byte values survive encode/decode
270        let data: Vec<u8> = (0..=255).collect();
271        let encoded = encode_frame(&data);
272        let decoded = decode_frame(&encoded[..encoded.len() - 1]);
273        assert_eq!(decoded, Some(data));
274    }
275
276    #[test]
277    fn config_response_cobs_roundtrip() {
278        // Exact Config response bytes: tag=1, freq=910525000, bw=62.5k, sf=7,
279        // cr=5, sync=0x3444, tx=22dBm, preamble=16, cad=1
280        // Note: preamble high byte is 0x00 — tests COBS with embedded zero.
281        let data = vec![0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05, 0x44, 0x34, 0x16, 0x10, 0x00, 0x01];
282        assert_eq!(data.len(), 14);
283        let encoded = encode_frame(&data);
284        let decoded = decode_frame(&encoded[..encoded.len() - 1]);
285        assert_eq!(decoded, Some(data));
286    }
287
288    #[test]
289    fn config_response_cobs_roundtrip_cad_zero() {
290        // Same but with cad=0 — two consecutive 0x00 bytes at end.
291        let data = vec![0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05, 0x44, 0x34, 0x16, 0x10, 0x00, 0x00];
292        assert_eq!(data.len(), 14);
293        let encoded = encode_frame(&data);
294        let decoded = decode_frame(&encoded[..encoded.len() - 1]);
295        assert_eq!(decoded, Some(data));
296    }
297
298    #[cfg(unix)]
299    #[test]
300    fn read_frame_socket_timeout_returns_none() {
301        use std::os::unix::net::UnixStream;
302        use std::time::Duration;
303
304        let (mut a, _b) = UnixStream::pair().unwrap();
305        a.set_read_timeout(Some(Duration::from_millis(10))).unwrap();
306
307        // No data written to _b, so read on `a` will time out with WouldBlock on Linux.
308        let result = read_frame(&mut a);
309        assert!(result.is_ok(), "socket timeout should be Ok(None), got: {result:?}");
310        assert!(result.unwrap().is_none());
311    }
312}