Skip to main content

haematite_cli/door/
forward.rs

1//! Studio's verbs and head follows, carried to their store's socket.
2//!
3//! The session's Lys pass is presented first (ACCESS-002 R2); a branch's
4//! head is followed the same way, the store pushing each change
5//! ([`follow`]).
6//!
7//! Each verb is one connection: hello, then `session.pass` with the
8//! session's pass, then the verb. A pass the store refuses ends the request
9//! there and the verb is never sent. `hello` touches no resource, so it is
10//! answered with the store's own hello. `session.pass` belongs to the door,
11//! so a browser can never present a pass of its own.
12
13use std::path::Path;
14use std::time::Duration;
15
16use serde_json::value::RawValue;
17use serde_json::{Value, json};
18
19use crate::client::{Client, Ended, Refusal, Reply};
20use crate::serve::pass::now;
21
22/// What a carried verb came to.
23#[derive(Debug)]
24pub enum Carried {
25    /// The bytes the store sent for `result`.
26    Answered(Box<RawValue>),
27    /// The store's refusal, or the door's for a verb it keeps to itself.
28    Refused(Refusal),
29}
30
31/// Carry `method` with `params` to the store at `socket`, presenting
32/// `token` first, each step bounded by `timeout`.
33///
34/// # Errors
35///
36/// [`Ended`] as the client names it: nothing asked, or, from the verb's
37/// sending on, an outcome that is not known.
38pub fn carry(
39    socket: &Path,
40    timeout: Duration,
41    token: &str,
42    method: &str,
43    params: Option<&RawValue>,
44) -> Result<Carried, Ended> {
45    if method == "session.pass" {
46        return Ok(Carried::Refused(Refusal {
47            code: "invalid_operation".to_owned(),
48            message: "session.pass is the door's own and is never carried".to_owned(),
49            path: None,
50            // The door refuses this itself; no store sent a receipt.
51            verified: None,
52        }));
53    }
54    let mut client = Client::connect(socket, timeout)?;
55    if method == "hello" {
56        return Ok(Carried::Answered(client.hello().to_owned()));
57    }
58    let pass = serde_json::value::to_raw_value(&json!({ "pass": token }))
59        .map_err(|error| Ended::NotAsked(format!("the pass could not be written: {error}")))?;
60    if let Reply::Refused(refusal) = client.call("session.pass", Some(&pass))? {
61        return Ok(Carried::Refused(refusal));
62    }
63    Ok(match client.call(method, params)? {
64        Reply::Answered(answer) => Carried::Answered(answer),
65        Reply::Refused(refusal) => Carried::Refused(refusal),
66    })
67}
68
69/// What asking a store to follow a branch's head came to.
70pub enum Followed {
71    /// The store took the follow and sent the head it starts from.
72    Following(Following),
73    /// The store refused the session's pass or the follow.
74    Refused(Refusal),
75}
76
77/// A follow a store took: the first head it sent, and the connection every
78/// later frame arrives on.
79pub struct Following {
80    /// The bytes the store sent for the first head's `result`.
81    pub first: Box<RawValue>,
82    client: Client,
83    /// When the presented pass expires, in seconds since the epoch, as the
84    /// store answered `session.pass`.
85    expires: Option<u64>,
86    timeout: Duration,
87}
88
89impl Following {
90    /// The store's next frame of the follow: a head, or the refusal that
91    /// ends it. The store sends a frame only when the head moves, so the
92    /// read waits as long as that takes, bounded by the pass's expiry and
93    /// `timeout` past it: the store ends every follow at that expiry, so a
94    /// read still waiting then means a store that stopped answering. With no
95    /// expiry known the read is unbounded.
96    ///
97    /// # Errors
98    ///
99    /// [`Ended`] as the client names it when no frame could be read.
100    pub fn next_frame(&mut self) -> Result<Reply, Ended> {
101        self.client.next_reply()
102    }
103
104    pub(super) fn disconnect_handle(&self) -> std::io::Result<std::os::unix::net::UnixStream> {
105        self.client.follow_socket()
106    }
107
108    /// Bound every later read by the pass's expiry and `timeout` past it,
109    /// set once when the follow starts. A store that has already closed the
110    /// connection refuses the bound on some systems (macOS answers EINVAL);
111    /// what it sent before closing is still read, and the read itself then
112    /// names the closed connection, so that refusal is not the follow's end.
113    fn bound_reads(&self) {
114        let bound = self.expires.map(|expires| {
115            let left = Duration::from_secs(expires.saturating_sub(now()));
116            left.saturating_add(self.timeout)
117        });
118        if let Err(ended) = self.client.read_within(bound) {
119            eprintln!("haematite door: a follow's read bound was not set: {ended}");
120        }
121    }
122}
123
124/// Ask the store at `socket` to follow `branch`'s head, presenting `token`
125/// first, each step up to the first head bounded by `timeout`.
126///
127/// # Errors
128///
129/// [`Ended`] as the client names it. A follow changes nothing, so an
130/// unknown outcome only means no head was read.
131pub fn follow(
132    socket: &Path,
133    timeout: Duration,
134    token: &str,
135    branch: &str,
136) -> Result<Followed, Ended> {
137    let mut client = Client::connect(socket, timeout)?;
138    let pass = raw(&json!({ "pass": token }))?;
139    let expires = match client.call("session.pass", Some(&pass))? {
140        Reply::Refused(refusal) => return Ok(Followed::Refused(refusal)),
141        Reply::Answered(answer) => expiry(&answer),
142    };
143    let params = raw(&json!({ "branch": branch }))?;
144    Ok(match client.call("head.follow", Some(&params))? {
145        Reply::Answered(first) => {
146            let following = Following {
147                first,
148                client,
149                expires,
150                timeout,
151            };
152            following.bound_reads();
153            Followed::Following(following)
154        }
155        Reply::Refused(refusal) => Followed::Refused(refusal),
156    })
157}
158
159fn raw(value: &Value) -> Result<Box<RawValue>, Ended> {
160    serde_json::value::to_raw_value(value)
161        .map_err(|error| Ended::NotAsked(format!("the request could not be written: {error}")))
162}
163
164/// The expiry, in seconds since the epoch, a store answered `session.pass`
165/// with: `{holder, expires}`.
166fn expiry(answer: &RawValue) -> Option<u64> {
167    let answer: Value = serde_json::from_str(answer.get()).ok()?;
168    answer.get("expires")?.as_u64()
169}
170
171#[cfg(test)]
172#[path = "forward_tests.rs"]
173mod tests;