summaryrefslogtreecommitdiff
path: root/crates/arti-rpc-client-core/src/llconn.rs
blob: 4473d7c6bab16d01b3cd62f9f428914a8a8c8672 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
//! Lowest-level API interface to an active RPC connection.
//!
//! Treats messages as unrelated strings, and validates outgoing messages for correctness.

use crate::{
    msgs::{
        request::{InvalidRequestError, ValidatedRequest},
        response::UnparsedResponse,
    },
    util::define_from_for_arc,
};
use std::{io, sync::Arc};

/// A low-level reader type, wrapping a boxed [`Read`](io::Read).
///
/// (Currently it performs no additional validation; instead it assumes
/// that Arti is obeying its specification.)
pub struct Reader {
    /// The underlying reader.
    backend: Box<dyn io::BufRead + Send>,
}

/// A low-level writer type, wrapping a boxed [`Write`](io::Write).
///
/// It enforces the property that outbound requests are syntactically well-formed.
pub struct Writer {
    /// The underlying writer.
    backend: Box<dyn io::Write + Send>,
}

impl Reader {
    /// Create a new Reader, wrapping an [`io::BufRead`].
    pub fn new<T>(backend: T) -> Self
    where
        T: io::BufRead + Send + 'static,
    {
        Self {
            backend: Box::new(backend),
        }
    }

    /// Receive an inbound reply.
    ///
    /// Blocks as needed until the reply is available.
    ///
    /// Returns `Ok(None)` on end-of-stream.
    pub fn read_msg(&mut self) -> io::Result<Option<UnparsedResponse>> {
        let mut s = String::new();

        // TODO: possibly ensure that the value is legit?
        match self.backend.read_line(&mut s) {
            Err(e) => Err(e),
            Ok(0) => Ok(None),
            Ok(_) if s.ends_with('\n') => Ok(Some(UnparsedResponse::new(s))),
            // NOTE: This can happen if we hit EOF.
            //
            // We discard any truncated lines in this case.
            Ok(_) => Ok(None),
        }
    }
}

impl Writer {
    /// Create a new writer, wrapping an [`io::Write`].
    pub fn new<T>(backend: T) -> Self
    where
        T: io::Write + Send + 'static,
    {
        Self {
            backend: Box::new(backend),
        }
    }

    /// Send an outbound request.
    ///
    /// Return an error if an IO problems occurred, or if the request was not well-formed.
    pub fn send_request(&mut self, request: &str) -> Result<(), SendRequestError> {
        let validated = ValidatedRequest::from_string_strict(request)?;
        self.send_valid(&validated)?;
        Ok(())
    }

    /// Crate-internal: Send a request that is known to be valid.
    ///
    /// (This is reliable since we never construct a `ValidRequest` except by encoding a
    /// known-correct object.)
    pub(crate) fn send_valid(&mut self, request: &ValidatedRequest) -> io::Result<()> {
        self.backend.write_all(request.as_ref().as_bytes())
    }

    /// Flush any queued data in this writer.
    pub fn flush(&mut self) -> io::Result<()> {
        self.backend.flush()
    }
}

/// An error that has occurred while sending a request.
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SendRequestError {
    /// An IO error occurred while sending a request.
    #[error("Unable to send request")]
    Io(#[source] Arc<io::Error>),
    /// We found a problem in the JSON while sending a request.
    #[error("Invalid Json request")]
    InvalidRequest(#[from] InvalidRequestError),
    /// Internal error while re-encoding request.  Should be impossible.
    #[error("Unable to re-encode request after parsing it‽")]
    ReEncode(#[source] Arc<serde_json::Error>),
}
define_from_for_arc!( io::Error => SendRequestError [Io] );

#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_duration_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->

    use std::thread;

    use io::{BufRead, BufReader, Cursor};

    use crate::util::assert_same_json;

    use super::*;

    struct NeverConnected;
    impl io::Read for NeverConnected {
        fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
            Err(io::ErrorKind::NotConnected.into())
        }
    }
    impl io::Write for NeverConnected {
        fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
            Err(io::ErrorKind::NotConnected.into())
        }

        fn flush(&mut self) -> io::Result<()> {
            Err(io::ErrorKind::NotConnected.into())
        }
    }

    #[test]
    fn reading() {
        // basic case: valid reply.
        let mut v = r#"{"id":7,"result":{}}"#.as_bytes().to_vec();
        v.push(b'\n');
        let mut r = Reader::new(Cursor::new(v));
        let m = r.read_msg();
        let msg = m.unwrap().unwrap();
        assert_eq!(
            msg.as_ref().strip_suffix('\n').unwrap(),
            r#"{"id":7,"result":{}}"#
        );

        // case 2: incomplete reply (gets treated as EOF)
        let mut r = Reader::new(Cursor::new(r#"{"id":7"#));
        let m = r.read_msg();
        assert!(m.unwrap().is_none());

        // Case 3: empty buffer (gets treated as EOF since there is no more to read.
        let mut r = Reader::new(Cursor::new(""));
        let m = r.read_msg();
        assert!(m.unwrap().is_none());

        // Case 4: reader gives an error
        let mut r = Reader::new(BufReader::new(NeverConnected));
        let m = r.read_msg();
        assert_eq!(m.unwrap_err().kind(), io::ErrorKind::NotConnected);
    }

    #[test]
    fn write_success() {
        let (r, w) = crate::testing::construct_socketpair().unwrap();
        let mut w = Writer::new(w);
        let mut r = io::BufReader::new(r);

        let wt: thread::JoinHandle<Result<(), SendRequestError>> = thread::spawn(move || {
            let res = w.send_request(
                r#"{"id":7,
                 "obj":"foo",
                 "method":"arti:x-frob", "params":{},
                 "extra": "preserved"
            }"#,
            );
            w.flush().unwrap();
            drop(w);
            res
        });
        let rt = thread::spawn(move || -> io::Result<String> {
            let mut s = String::new();
            r.read_line(&mut s)?;
            Ok(s)
        });
        let write_result = wt.join().unwrap();
        assert!(write_result.is_ok());
        let read_result = rt.join().unwrap().unwrap();
        assert_same_json!(
            read_result.strip_suffix('\n').unwrap(),
            r#"{"id":7,"obj":"foo","method":"arti:x-frob","params":{},"extra":"preserved"}"#
        );
    }

    #[test]
    fn write_failure() {
        let mut w = Writer::new(NeverConnected);

        // Write an incomplete request.
        assert!(matches!(
            w.send_request("{"),
            Err(SendRequestError::InvalidRequest(_))
        ));

        // Write an invalid request.
        assert!(matches!(
            w.send_request("{}"),
            Err(SendRequestError::InvalidRequest(_))
        ));

        // Valid request, but get an IO error.
        let r = w.send_request(r#"{"id":7,"obj":"foo","method":"arti:x-frob","params":{}}"#);
        assert!(
            matches!(r, Err(SendRequestError::Io(e)) if e.kind() == io::ErrorKind::NotConnected)
        );
    }
}