summaryrefslogtreecommitdiffstats
path: root/src/protocol.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/protocol.rs')
-rw-r--r--src/protocol.rs70
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
6use std::io::{self, Read};
7
8#[derive(Debug, Default)]
9pub struct LineBuffer {
10 buffer: Vec<u8>,
11}
12
13impl 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.
65pub 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}