diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-28 22:38:54 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-28 22:38:54 +0100 |
| commit | ef2e13df274aaf0f2e50c03c63db33d65d9a0275 (patch) | |
| tree | 991fa12e70ec11e84e8f6c6e7bc0a60b0a150759 /src/protocol.rs | |
| parent | v0.2.5: fix terminal width, ping timing and output formatting (diff) | |
| download | zagan-bet-ef2e13df274aaf0f2e50c03c63db33d65d9a0275.tar.gz zagan-bet-ef2e13df274aaf0f2e50c03c63db33d65d9a0275.zip | |
v0.2.6: preserve buffered lines and add request read timeouts
Diffstat (limited to 'src/protocol.rs')
| -rw-r--r-- | src/protocol.rs | 70 |
1 files changed, 70 insertions, 0 deletions
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 @@ | |||
| 1 | //! Newline-delimited protocol helpers shared by the client and server. | ||
| 2 | //! | ||
| 3 | //! TLS reads can return more than one line at a time. Keeping the leftover | ||
| 4 | //! bytes is essential: dropping them silently loses the next message. | ||
| 5 | |||
| 6 | use std::io::{self, Read}; | ||
| 7 | |||
| 8 | #[derive(Debug, Default)] | ||
| 9 | pub struct LineBuffer { | ||
| 10 | buffer: Vec<u8>, | ||
| 11 | } | ||
| 12 | |||
| 13 | impl LineBuffer { | ||
| 14 | pub fn new() -> Self { | ||
| 15 | Self::default() | ||
| 16 | } | ||
| 17 | |||
| 18 | pub fn push(&mut self, bytes: &[u8]) { | ||
| 19 | self.buffer.extend_from_slice(bytes); | ||
| 20 | } | ||
| 21 | |||
| 22 | /// Return the next complete line, including a line that ended at EOF. | ||
| 23 | pub fn next_line(&mut self) -> Option<String> { | ||
| 24 | let end = self.buffer.iter().position(|byte| *byte == b'\n')?; | ||
| 25 | let mut line: Vec<u8> = self.buffer.drain(..=end).collect(); | ||
| 26 | line.pop(); // '\n' | ||
| 27 | if line.last() == Some(&b'\r') { | ||
| 28 | line.pop(); | ||
| 29 | } | ||
| 30 | Some(String::from_utf8_lossy(&line).into_owned()) | ||
| 31 | } | ||
| 32 | |||
| 33 | /// Return the unterminated remainder at EOF, if any. | ||
| 34 | pub fn take_rest(&mut self) -> Option<String> { | ||
| 35 | if self.buffer.is_empty() { | ||
| 36 | return None; | ||
| 37 | } | ||
| 38 | let mut rest = std::mem::take(&mut self.buffer); | ||
| 39 | if rest.last() == Some(&b'\r') { | ||
| 40 | rest.pop(); | ||
| 41 | } | ||
| 42 | Some(String::from_utf8_lossy(&rest).into_owned()) | ||
| 43 | } | ||
| 44 | |||
| 45 | /// Read one line from a blocking reader, preserving any extra lines for | ||
| 46 | /// the next call. Returns `None` at EOF with nothing buffered. | ||
| 47 | pub fn read_line<R: Read>(&mut self, reader: &mut R) -> io::Result<Option<String>> { | ||
| 48 | loop { | ||
| 49 | if let Some(line) = self.next_line() { | ||
| 50 | return Ok(Some(line)); | ||
| 51 | } | ||
| 52 | |||
| 53 | let mut chunk = [0u8; 4096]; | ||
| 54 | match reader.read(&mut chunk) { | ||
| 55 | Ok(0) => return Ok(self.take_rest()), | ||
| 56 | Ok(n) => self.push(&chunk[..n]), | ||
| 57 | Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, | ||
| 58 | Err(error) => return Err(error), | ||
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | /// A stable, user-facing message for a failed request. | ||
| 65 | pub fn io_failure_message(error: &io::Error) -> &'static str { | ||
| 66 | match error.kind() { | ||
| 67 | io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => "ERROR Request timed out", | ||
| 68 | _ => "ERROR Connection lost", | ||
| 69 | } | ||
| 70 | } | ||
