From ef2e13df274aaf0f2e50c03c63db33d65d9a0275 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 28 Oct 2026 22:38:54 +0100 Subject: v0.2.6: preserve buffered lines and add request read timeouts --- src/protocol.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/protocol.rs (limited to 'src/protocol.rs') diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 0000000..11f54a1 --- /dev/null +++ b/src/protocol.rs @@ -0,0 +1,70 @@ +//! Newline-delimited protocol helpers shared by the client and server. +//! +//! TLS reads can return more than one line at a time. Keeping the leftover +//! bytes is essential: dropping them silently loses the next message. + +use std::io::{self, Read}; + +#[derive(Debug, Default)] +pub struct LineBuffer { + buffer: Vec, +} + +impl LineBuffer { + pub fn new() -> Self { + Self::default() + } + + pub fn push(&mut self, bytes: &[u8]) { + self.buffer.extend_from_slice(bytes); + } + + /// Return the next complete line, including a line that ended at EOF. + pub fn next_line(&mut self) -> Option { + let end = self.buffer.iter().position(|byte| *byte == b'\n')?; + let mut line: Vec = self.buffer.drain(..=end).collect(); + line.pop(); // '\n' + if line.last() == Some(&b'\r') { + line.pop(); + } + Some(String::from_utf8_lossy(&line).into_owned()) + } + + /// Return the unterminated remainder at EOF, if any. + pub fn take_rest(&mut self) -> Option { + if self.buffer.is_empty() { + return None; + } + let mut rest = std::mem::take(&mut self.buffer); + if rest.last() == Some(&b'\r') { + rest.pop(); + } + Some(String::from_utf8_lossy(&rest).into_owned()) + } + + /// Read one line from a blocking reader, preserving any extra lines for + /// the next call. Returns `None` at EOF with nothing buffered. + pub fn read_line(&mut self, reader: &mut R) -> io::Result> { + loop { + if let Some(line) = self.next_line() { + return Ok(Some(line)); + } + + let mut chunk = [0u8; 4096]; + match reader.read(&mut chunk) { + Ok(0) => return Ok(self.take_rest()), + Ok(n) => self.push(&chunk[..n]), + Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, + Err(error) => return Err(error), + } + } + } +} + +/// A stable, user-facing message for a failed request. +pub fn io_failure_message(error: &io::Error) -> &'static str { + match error.kind() { + io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => "ERROR Request timed out", + _ => "ERROR Connection lost", + } +} -- cgit v1.2.3