lager-net 0.8.0

First-class Rust client for Lager box nets: drive power supplies, batteries, e-loads, GPIO, ADC, DAC, SPI, I2C, UART and more from cargo tests.
Documentation
//! Decoding for the debug service's hand-framed chunked RTT stream.
//!
//! The box debug service (`box/lager/debug/service.py`) answers
//! `/debug/rtt` with an `HTTP/1.0` status line, yet sends
//! `Transfer-Encoding: chunked` and writes the chunk framing itself.
//! Chunked transfer coding does not exist in HTTP/1.0, so ureq rightly
//! passes the body through untouched, framing and all. [`ChunkedReader`]
//! strips that framing so callers see only the RTT payload.

use std::io::{self, BufRead, BufReader, Read};

/// Longest chunk-size line accepted, extensions included. The box sends
/// at most a few hex digits; anything this long is not chunk framing.
const MAX_SIZE_LINE: usize = 1024;

enum State {
    /// Expecting a chunk-size line.
    Size,
    /// Inside a chunk with this many payload bytes left.
    Data(u64),
    /// Expecting the CRLF that closes a chunk's payload.
    DataEnd,
    /// The terminating zero-size chunk was seen, or the stream closed
    /// between chunks.
    Done,
}

/// A reader that decodes an HTTP chunked body.
///
/// The stream ends at the zero-size chunk. It also ends cleanly if the
/// connection closes between chunks: a live log stream can be cut off
/// without its terminator, and nothing is lost when that happens on a
/// chunk boundary. A close inside a chunk is
/// [`io::ErrorKind::UnexpectedEof`], since payload bytes are missing.
pub(crate) struct ChunkedReader<R> {
    inner: BufReader<R>,
    state: State,
}

impl<R: Read> ChunkedReader<R> {
    pub(crate) fn new(inner: R) -> Self {
        ChunkedReader {
            inner: BufReader::new(inner),
            state: State::Size,
        }
    }

    /// Read one CRLF- (or bare LF-) terminated line, without the
    /// terminator. `None` means the stream closed before any byte of it.
    fn read_line(&mut self) -> io::Result<Option<Vec<u8>>> {
        let mut line = Vec::new();
        let n = (&mut self.inner)
            .take(MAX_SIZE_LINE as u64 + 2)
            .read_until(b'\n', &mut line)?;
        if n == 0 {
            return Ok(None);
        }
        if line.last() != Some(&b'\n') {
            return Err(if n > MAX_SIZE_LINE {
                invalid("chunk-size line too long")
            } else {
                unexpected_eof("stream closed inside a chunk-size line")
            });
        }
        line.pop();
        if line.last() == Some(&b'\r') {
            line.pop();
        }
        Ok(Some(line))
    }

    /// Parse a chunk-size line: hex digits, optionally followed by
    /// `;extension`s, which are ignored.
    fn parse_size(line: &[u8]) -> io::Result<u64> {
        let digits = line.split(|&b| b == b';').next().unwrap_or_default();
        let digits = std::str::from_utf8(digits)
            .map_err(|_| invalid("chunk size is not ASCII"))?
            .trim();
        u64::from_str_radix(digits, 16).map_err(|_| invalid("chunk size is not hex"))
    }

    /// Skip the trailer section that follows the zero-size chunk.
    fn skip_trailers(&mut self) -> io::Result<()> {
        while let Some(line) = self.read_line()? {
            if line.is_empty() {
                break;
            }
        }
        Ok(())
    }
}

impl<R: Read> Read for ChunkedReader<R> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        if buf.is_empty() {
            return Ok(0);
        }
        loop {
            match self.state {
                State::Done => return Ok(0),
                State::Size => {
                    let Some(line) = self.read_line()? else {
                        self.state = State::Done;
                        return Ok(0);
                    };
                    match Self::parse_size(&line)? {
                        0 => {
                            self.skip_trailers()?;
                            self.state = State::Done;
                        }
                        size => self.state = State::Data(size),
                    }
                }
                State::Data(left) => {
                    let want = buf.len().min(usize::try_from(left).unwrap_or(usize::MAX));
                    let n = self.inner.read(&mut buf[..want])?;
                    if n == 0 {
                        return Err(unexpected_eof("stream closed inside a chunk"));
                    }
                    let left = left - n as u64;
                    self.state = if left == 0 {
                        State::DataEnd
                    } else {
                        State::Data(left)
                    };
                    return Ok(n);
                }
                State::DataEnd => match self.read_line()? {
                    Some(line) if line.is_empty() => self.state = State::Size,
                    Some(_) => return Err(invalid("chunk payload longer than its size")),
                    None => return Err(unexpected_eof("stream closed before a chunk's CRLF")),
                },
            }
        }
    }
}

fn invalid(msg: &str) -> io::Error {
    io::Error::new(io::ErrorKind::InvalidData, msg)
}

fn unexpected_eof(msg: &str) -> io::Error {
    io::Error::new(io::ErrorKind::UnexpectedEof, msg)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A reader that hands out at most `step` bytes per call, to exercise
    /// framing split across reads.
    struct Trickle<'a> {
        data: &'a [u8],
        step: usize,
    }

    impl Read for Trickle<'_> {
        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
            let n = self.step.min(buf.len()).min(self.data.len());
            buf[..n].copy_from_slice(&self.data[..n]);
            self.data = &self.data[n..];
            Ok(n)
        }
    }

    fn decode(body: &[u8]) -> io::Result<Vec<u8>> {
        let mut out = Vec::new();
        ChunkedReader::new(body).read_to_end(&mut out)?;
        Ok(out)
    }

    #[test]
    fn strips_framing() {
        let body = b"19\r\nblink 46823 period=500ms\n\r\n4\r\nnext\r\n0\r\n\r\n";
        assert_eq!(decode(body).unwrap(), b"blink 46823 period=500ms\nnext");
    }

    #[test]
    fn decodes_byte_at_a_time() {
        let body = b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
        let mut out = Vec::new();
        ChunkedReader::new(Trickle {
            data: body,
            step: 1,
        })
        .read_to_end(&mut out)
        .unwrap();
        assert_eq!(out, b"hello world");
    }

    #[test]
    fn small_caller_buffer_spans_a_chunk() {
        let mut r = ChunkedReader::new(&b"a\r\n0123456789\r\n0\r\n\r\n"[..]);
        let mut buf = [0u8; 4];
        let mut out = Vec::new();
        loop {
            let n = r.read(&mut buf).unwrap();
            if n == 0 {
                break;
            }
            out.extend_from_slice(&buf[..n]);
        }
        assert_eq!(out, b"0123456789");
    }

    #[test]
    fn payload_may_contain_crlf_and_hex_lookalikes() {
        // The firmware's own CRLFs and hex-looking lines are payload, not
        // framing: only the size prefix decides where a chunk ends.
        let body = b"8\r\n64\r\n\r\nff\r\n0\r\n\r\n";
        assert_eq!(decode(body).unwrap(), b"64\r\n\r\nff");
    }

    #[test]
    fn accepts_uppercase_hex_extensions_and_trailers() {
        let body = b"A;name=val\r\n0123456789\r\n0\r\nX-Trailer: 1\r\n\r\n";
        assert_eq!(decode(body).unwrap(), b"0123456789");
    }

    #[test]
    fn accepts_bare_lf() {
        assert_eq!(decode(b"3\nabc\n0\n\n").unwrap(), b"abc");
    }

    #[test]
    fn close_between_chunks_is_a_clean_end() {
        // The box's terminator is sent from a `finally`, but a killed
        // process or dropped link never sends it.
        assert_eq!(decode(b"3\r\nabc\r\n").unwrap(), b"abc");
        assert_eq!(decode(b"").unwrap(), b"");
    }

    #[test]
    fn close_inside_a_chunk_is_an_error() {
        let err = decode(b"a\r\nabc").unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
        let err = decode(b"3\r\nabc").unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
        let err = decode(b"3").unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
    }

    #[test]
    fn rejects_bad_framing() {
        assert_eq!(
            decode(b"zz\r\nabc\r\n").unwrap_err().kind(),
            io::ErrorKind::InvalidData
        );
        assert_eq!(
            decode(b"2\r\nabc\r\n").unwrap_err().kind(),
            io::ErrorKind::InvalidData
        );
        let long = vec![b'1'; MAX_SIZE_LINE + 10];
        assert_eq!(
            decode(&long).unwrap_err().kind(),
            io::ErrorKind::InvalidData
        );
    }

    #[test]
    fn nothing_after_the_terminator_is_read() {
        let body = b"3\r\nabc\r\n0\r\n\r\ngarbage";
        assert_eq!(decode(body).unwrap(), b"abc");
    }
}