Skip to main content

whipplescript_custody/
client.rs

1//! whip's client transport to a custodian daemon on this box: one
2//! newline-delimited JSON call per connection over a Unix domain socket
3//! (the server half lives in `whipplescript-custodian::serve`). Unix-only,
4//! so the protocol crate stays wasm-clean; a wasm host supplies its own
5//! [`CustodyTransport`].
6
7use std::io::{BufRead, BufReader, Read, Write};
8use std::os::unix::net::UnixStream;
9use std::path::PathBuf;
10
11use crate::{CustodyCall, CustodyReply, CustodyTransport, TransportError};
12
13/// Matches the daemon's per-line cap.
14const MAX_LINE_BYTES: usize = 4 * 1024 * 1024;
15
16/// The variable is crate-root vocabulary — a non-Unix build has no transport but
17/// still needs to know whether a custodian was asked for — and is re-exported
18/// here because this is where a reader of the transport looks for it.
19pub use crate::CUSTODIAN_SOCKET_ENV;
20
21pub struct UnixSocketTransport {
22    socket_path: PathBuf,
23}
24
25impl UnixSocketTransport {
26    pub fn new(socket_path: PathBuf) -> Self {
27        Self { socket_path }
28    }
29
30    /// The transport named by `WHIPPLESCRIPT_CUSTODIAN_SOCKET`, if set.
31    pub fn from_env() -> Option<Self> {
32        std::env::var_os(CUSTODIAN_SOCKET_ENV).map(|path| Self::new(PathBuf::from(path)))
33    }
34}
35
36impl CustodyTransport for UnixSocketTransport {
37    fn call(&self, call: CustodyCall) -> Result<CustodyReply, TransportError> {
38        let stream = UnixStream::connect(&self.socket_path)
39            .map_err(|e| TransportError::Unavailable(e.to_string()))?;
40        let mut writer = stream
41            .try_clone()
42            .map_err(|e| TransportError::Unavailable(e.to_string()))?;
43        let wire = serde_json::to_string(&call)
44            .map_err(|e| TransportError::Protocol(format!("unserializable call: {e}")))?;
45        writer
46            .write_all(wire.as_bytes())
47            .and_then(|_| writer.write_all(b"\n"))
48            .map_err(|e| TransportError::Unavailable(e.to_string()))?;
49        let mut reader = BufReader::new(stream).take(MAX_LINE_BYTES as u64);
50        let mut line = String::new();
51        reader
52            .read_line(&mut line)
53            .map_err(|e| TransportError::Unavailable(e.to_string()))?;
54        let value: serde_json::Value = serde_json::from_str(line.trim())
55            .map_err(|e| TransportError::Protocol(format!("malformed reply: {e}")))?;
56        if let Some(detail) = value.get("protocol_error").and_then(|v| v.as_str()) {
57            return Err(TransportError::Protocol(detail.to_string()));
58        }
59        serde_json::from_value(value)
60            .map_err(|e| TransportError::Protocol(format!("malformed reply: {e}")))
61    }
62}