1use std::fmt;
29use std::io::{self, Read, Write};
30use std::path::PathBuf;
31
32pub const MAX_FRAME: usize = 64 * 1024;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Voice {
51 Client,
53 Instance,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum Request {
60 Open {
62 container: PathBuf,
63 voice: Voice,
65 },
66 List,
68 Close(String),
70 Ping,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum Response {
77 Ok(Vec<String>),
79 Err(String),
81}
82
83#[derive(Debug)]
85pub enum Error {
86 Io(io::Error),
88 TooLong(usize),
90 Malformed(String),
92}
93
94impl fmt::Display for Error {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 match self {
97 Self::Io(e) => write!(f, "{e}"),
98 Self::TooLong(n) => write!(f, "a frame of {n} bytes is longer than {MAX_FRAME}"),
99 Self::Malformed(what) => write!(f, "unintelligible frame: {what}"),
100 }
101 }
102}
103
104impl std::error::Error for Error {}
105
106impl From<io::Error> for Error {
107 fn from(e: io::Error) -> Self {
108 Self::Io(e)
109 }
110}
111
112impl Request {
113 fn fields(&self) -> Vec<Vec<u8>> {
114 match self {
115 Self::Open { container, voice } => vec![
116 b"open".to_vec(),
117 path_bytes(container),
118 match voice {
119 Voice::Client => b"client".to_vec(),
120 Voice::Instance => b"instance".to_vec(),
121 },
122 ],
123 Self::List => vec![b"list".to_vec()],
124 Self::Close(id) => vec![b"close".to_vec(), id.as_bytes().to_vec()],
125 Self::Ping => vec![b"ping".to_vec()],
126 }
127 }
128
129 fn from_fields(fields: &[Vec<u8>]) -> Result<Self, Error> {
130 let verb = fields.first().map(Vec::as_slice).unwrap_or_default();
131 match (verb, fields.len()) {
132 (b"open", 3) => Ok(Self::Open {
133 container: path_from(&fields[1]),
134 voice: match fields[2].as_slice() {
135 b"client" => Voice::Client,
136 b"instance" => Voice::Instance,
137 other => {
138 return Err(Error::Malformed(format!(
139 "open with an unknown voice {}",
140 String::from_utf8_lossy(other)
141 )))
142 }
143 },
144 }),
145 (b"list", 1) => Ok(Self::List),
146 (b"close", 2) => Ok(Self::Close(text(&fields[1]))),
147 (b"ping", 1) => Ok(Self::Ping),
148 _ => Err(Error::Malformed(format!(
149 "{} with {} field(s)",
150 String::from_utf8_lossy(verb),
151 fields.len()
152 ))),
153 }
154 }
155}
156
157impl Response {
158 fn fields(&self) -> Vec<Vec<u8>> {
159 match self {
160 Self::Ok(lines) => std::iter::once(b"ok".to_vec())
161 .chain(lines.iter().map(|l| l.as_bytes().to_vec()))
162 .collect(),
163 Self::Err(why) => vec![b"err".to_vec(), why.as_bytes().to_vec()],
164 }
165 }
166
167 fn from_fields(fields: &[Vec<u8>]) -> Result<Self, Error> {
168 match fields.first().map(Vec::as_slice) {
169 Some(b"ok") => Ok(Self::Ok(fields[1..].iter().map(|f| text(f)).collect())),
170 Some(b"err") if fields.len() == 2 => Ok(Self::Err(text(&fields[1]))),
171 other => Err(Error::Malformed(format!(
172 "{}",
173 String::from_utf8_lossy(other.unwrap_or_default())
174 ))),
175 }
176 }
177}
178
179#[cfg(unix)]
182fn path_bytes(path: &std::path::Path) -> Vec<u8> {
183 use std::os::unix::ffi::OsStrExt as _;
184 path.as_os_str().as_bytes().to_vec()
185}
186
187#[cfg(unix)]
188fn path_from(bytes: &[u8]) -> PathBuf {
189 use std::os::unix::ffi::OsStringExt as _;
190 PathBuf::from(std::ffi::OsString::from_vec(bytes.to_vec()))
191}
192
193#[cfg(not(unix))]
194fn path_bytes(path: &std::path::Path) -> Vec<u8> {
195 path.to_string_lossy().into_owned().into_bytes()
196}
197
198#[cfg(not(unix))]
199fn path_from(bytes: &[u8]) -> PathBuf {
200 PathBuf::from(String::from_utf8_lossy(bytes).into_owned())
201}
202
203fn text(bytes: &[u8]) -> String {
204 String::from_utf8_lossy(bytes).into_owned()
205}
206
207fn write_frame(to: &mut impl Write, fields: &[Vec<u8>]) -> Result<(), Error> {
208 let body = fields.join(&0u8);
209 let len = u32::try_from(body.len()).map_err(|_| Error::TooLong(body.len()))?;
210 if body.len() > MAX_FRAME {
211 return Err(Error::TooLong(body.len()));
212 }
213 to.write_all(&len.to_be_bytes())?;
214 to.write_all(&body)?;
215 to.flush()?;
216 Ok(())
217}
218
219fn read_frame(from: &mut impl Read) -> Result<Vec<Vec<u8>>, Error> {
220 let mut len = [0u8; 4];
221 from.read_exact(&mut len)?;
222 let len = u32::from_be_bytes(len) as usize;
223 if len > MAX_FRAME {
226 return Err(Error::TooLong(len));
227 }
228 let mut body = vec![0u8; len];
229 from.read_exact(&mut body)?;
230 Ok(body.split(|b| *b == 0).map(<[u8]>::to_vec).collect())
231}
232
233pub fn ask(stream: &mut (impl Read + Write), request: &Request) -> Result<Response, Error> {
239 write_frame(stream, &request.fields())?;
240 Response::from_fields(&read_frame(stream)?)
241}
242
243pub fn take(stream: &mut impl Read) -> Result<Request, Error> {
249 Request::from_fields(&read_frame(stream)?)
250}
251
252pub fn answer(stream: &mut impl Write, response: &Response) -> Result<(), Error> {
258 write_frame(stream, &response.fields())
259}
260
261#[cfg(test)]
262mod tests {
263 use super::{answer, ask, take, Error, Request, Response, Voice};
264 use std::io::Cursor;
265 use std::path::PathBuf;
266
267 struct Pair {
270 to_them: Vec<u8>,
271 from_them: Cursor<Vec<u8>>,
272 }
273
274 impl std::io::Write for Pair {
275 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
276 self.to_them.write(buf)
277 }
278 fn flush(&mut self) -> std::io::Result<()> {
279 Ok(())
280 }
281 }
282
283 impl std::io::Read for Pair {
284 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
285 std::io::Read::read(&mut self.from_them, buf)
286 }
287 }
288
289 fn round_trip(request: &Request) -> Request {
290 let mut wire = Vec::new();
291 let mut out = Pair {
292 to_them: Vec::new(),
293 from_them: Cursor::new(Vec::new()),
294 };
295 super::write_frame(&mut out, &request.fields()).unwrap();
296 wire.extend_from_slice(&out.to_them);
297 take(&mut Cursor::new(wire)).unwrap()
298 }
299
300 #[test]
301 fn every_request_survives_the_wire() {
302 for r in [
303 Request::Open {
304 container: PathBuf::from("/tmp/report.slpc"),
305 voice: Voice::Client,
306 },
307 Request::List,
308 Request::Close("6a94-0".to_string()),
309 Request::Ping,
310 ] {
311 assert_eq!(round_trip(&r), r, "{r:?}");
312 }
313 }
314
315 #[cfg(unix)]
316 #[test]
317 fn a_path_a_line_protocol_would_break_survives() {
318 for name in [
322 "/tmp/two\tcolumns.slpc",
323 "/tmp/two\nlines.slpc",
324 "/tmp/a \"quoted\" name.slpc",
325 ] {
326 let r = Request::Open {
327 container: PathBuf::from(name),
328 voice: Voice::Instance,
329 };
330 assert_eq!(round_trip(&r), r, "{name}");
331 }
332 }
333
334 #[cfg(unix)]
335 #[test]
336 fn a_path_that_is_not_utf8_survives() {
337 use std::os::unix::ffi::OsStringExt as _;
341 let name = PathBuf::from(std::ffi::OsString::from_vec(vec![
342 b'/', b't', b'm', b'p', b'/', 0xff, 0xfe, b'.', b's', b'l', b'p', b'c',
343 ]));
344 let r = Request::Open {
345 container: name,
346 voice: Voice::Client,
347 };
348 assert_eq!(round_trip(&r), r);
349 }
350
351 #[test]
352 fn a_response_survives_the_wire() {
353 let mut wire = Vec::new();
354 answer(&mut wire, &Response::Ok(vec!["one".into(), "two".into()])).unwrap();
355 let mut c = Cursor::new(wire);
356 let mut both = Pair {
357 to_them: Vec::new(),
358 from_them: Cursor::new(Vec::new()),
359 };
360 std::io::copy(&mut c, &mut both.to_them).unwrap();
361 both.from_them = Cursor::new(both.to_them.clone());
362 let fields = super::read_frame(&mut both.from_them).unwrap();
364 assert_eq!(
365 super::Response::from_fields(&fields).unwrap(),
366 Response::Ok(vec!["one".into(), "two".into()])
367 );
368 }
369
370 #[test]
371 fn an_error_response_carries_its_reason() {
372 let mut wire = Vec::new();
373 answer(&mut wire, &Response::Err("pdf is on the deny list".into())).unwrap();
374 let fields = super::read_frame(&mut Cursor::new(wire)).unwrap();
375 assert_eq!(
376 super::Response::from_fields(&fields).unwrap(),
377 Response::Err("pdf is on the deny list".into())
378 );
379 }
380
381 #[test]
382 fn a_frame_longer_than_the_cap_is_refused_before_it_is_allocated() {
383 let mut wire = Vec::new();
386 wire.extend_from_slice(&u32::MAX.to_be_bytes());
387 match super::read_frame(&mut Cursor::new(wire)) {
388 Err(Error::TooLong(n)) => assert_eq!(n, u32::MAX as usize),
389 other => panic!("{other:?}"),
390 }
391 }
392
393 #[test]
394 fn a_verb_this_build_does_not_know_is_refused_rather_than_guessed() {
395 let mut wire = Vec::new();
396 super::write_frame(&mut wire, &[b"drop-everything".to_vec()]).unwrap();
397 assert!(matches!(
398 take(&mut Cursor::new(wire)),
399 Err(Error::Malformed(_))
400 ));
401 }
402
403 #[test]
404 fn a_known_verb_with_the_wrong_shape_is_refused() {
405 for fields in [
409 vec![b"open".to_vec()],
410 vec![b"close".to_vec(), b"a".to_vec(), b"b".to_vec()],
411 ] {
412 let mut wire = Vec::new();
413 super::write_frame(&mut wire, &fields).unwrap();
414 assert!(matches!(
415 take(&mut Cursor::new(wire)),
416 Err(Error::Malformed(_))
417 ));
418 }
419 }
420
421 #[test]
422 fn a_connection_that_ends_mid_frame_is_an_error_and_not_a_hang() {
423 let mut wire = Vec::new();
424 super::write_frame(&mut wire, &Request::List.fields()).unwrap();
425 wire.truncate(wire.len() - 1);
426 assert!(matches!(take(&mut Cursor::new(wire)), Err(Error::Io(_))));
427 }
428
429 #[test]
430 fn asking_writes_a_request_and_reads_the_answer() {
431 let mut server_side = Vec::new();
432 answer(&mut server_side, &Response::Ok(vec!["fine".into()])).unwrap();
433 let mut pair = Pair {
434 to_them: Vec::new(),
435 from_them: Cursor::new(server_side),
436 };
437 assert_eq!(
438 ask(&mut pair, &Request::Ping).unwrap(),
439 Response::Ok(vec!["fine".into()])
440 );
441 assert_eq!(take(&mut Cursor::new(pair.to_them)).unwrap(), Request::Ping);
443 }
444}