Skip to main content

bash_interop/rig/wire/
message.rs

1//! What travels: a shell's account of itself, the messages it sends, and the
2//! answers it is sent.
3//!
4//! Each is a bash array literal. The protocol puts its own words in front — a
5//! verb where there is one, then the sending shell's clock as `at=` — and the
6//! reader shifts exactly those back off, leaving what the shell wrote.
7
8use std::fmt;
9use std::time::{SystemTime, UNIX_EPOCH};
10use std::vec;
11
12use serde::{Deserialize, Serialize};
13
14use crate::failure::{Doing, Failure};
15use bash_strings::{emit_array, parse_array};
16
17#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
18pub struct Micros(pub u64);
19
20impl Micros {
21    /// The run's own clock, to sit beside the sending shell's `$EPOCHREALTIME`.
22    pub(crate) fn now() -> Result<Self, Failure> {
23        SystemTime::now()
24            .duration_since(UNIX_EPOCH)
25            .map(|since| Self(since.as_micros() as u64))
26            .doing(|| "reading the run's clock".into())
27    }
28
29    /// `$EPOCHREALTIME`: seconds, the locale's decimal separator, six digits.
30    fn parse_epoch(text: &str) -> Option<Self> {
31        let (seconds, micros) = text.split_once(['.', ','])?;
32        if micros.len() != 6 {
33            return None;
34        }
35
36        Some(Self(
37            seconds.parse::<u64>().ok()? * 1_000_000 + micros.parse::<u64>().ok()?,
38        ))
39    }
40}
41
42#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
43pub struct Pid(pub u32);
44
45impl fmt::Display for Pid {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(f, "{}", self.0)
48    }
49}
50
51/// Whether the shell is waiting for something back. A word outside this set is
52/// a defect in the bash, never a client's choice: a client's own tag is a
53/// payload word, and the protocol never reads one.
54#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize)]
55#[serde(rename_all = "UPPERCASE")]
56pub enum Verb {
57    Say,
58    Ask,
59}
60
61/// When one line was written and when it was read.
62#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
63pub struct Stamp {
64    /// The sending shell's `$EPOCHREALTIME`.
65    pub sent_at: Micros,
66
67    /// The run's clock at the read that completed the line.
68    pub heard_at: Micros,
69}
70
71/// What one shell's client said, once.
72#[derive(Debug, Clone, Serialize)]
73pub struct Message {
74    pub verb: Verb,
75    pub stamp: Stamp,
76
77    /// The client's arglist, and nothing of the protocol's.
78    pub words: Vec<String>,
79}
80
81impl Message {
82    /// The words after `lead`, if this message begins with it — how a decoder
83    /// claims one family of messages and declines the rest.
84    pub fn behind(&self, lead: &str) -> Option<&[String]> {
85        match self.words.split_first() {
86            Some((first, rest)) if first == lead => Some(rest),
87            _ => None,
88        }
89    }
90
91    /// One line off a shell's pipe: the verb, the clock, the words.
92    pub(crate) fn read(line: Line) -> Result<Self, Failure> {
93        let refused = |why: &str| {
94            Failure::new(
95                format!("reading the line {:?}", line.text),
96                why,
97            )
98        };
99        let mut ahead = Ahead::over(&line.text).map_err(|why| refused(&why))?;
100
101        let verb = match ahead.word().map_err(refused)?.as_str() {
102            "SAY" => Verb::Say,
103            "ASK" => Verb::Ask,
104            other => {
105                return Err(refused(&format!(
106                    "{other} is not a verb"
107                )));
108            }
109        };
110        let sent_at = ahead.clock().map_err(refused)?;
111
112        Ok(Self {
113            verb,
114            stamp: Stamp {
115                sent_at,
116                heard_at: line.heard_at,
117            },
118            words: ahead.rest(),
119        })
120    }
121}
122
123/// A shell's account of itself, as it announces itself: the clock, then the
124/// pairs [`Shell::of`](crate::shell::Shell::of) reads.
125#[derive(Debug)]
126pub(crate) struct Account {
127    pub stamp: Stamp,
128    pub words: Vec<String>,
129}
130
131impl Account {
132    pub(crate) fn read(text: &str, heard_at: Micros) -> Result<Self, Failure> {
133        let refused = |why: &str| {
134            Failure::new(
135                format!("reading the account {text:?}"),
136                why,
137            )
138        };
139        let mut ahead = Ahead::over(text).map_err(|why| refused(&why))?;
140        let sent_at = ahead.clock().map_err(refused)?;
141
142        Ok(Self {
143            stamp: Stamp { sent_at, heard_at },
144            words: ahead.rest(),
145        })
146    }
147}
148
149/// One line as read off a shell's pipe, with the run's clock at the read.
150#[derive(Debug)]
151pub(crate) struct Line {
152    pub text: String,
153    pub heard_at: Micros,
154}
155
156/// The protocol's own words, taken off the front one at a time.
157struct Ahead(vec::IntoIter<String>);
158
159impl Ahead {
160    fn over(text: &str) -> Result<Self, String> {
161        parse_array(text)
162            .map(|words| Self(words.into_iter()))
163            .map_err(|why| why.to_string())
164    }
165
166    fn word(&mut self) -> Result<String, &'static str> {
167        self.0.next().ok_or("the line ended early")
168    }
169
170    /// The `at=` header: the sender's `$EPOCHREALTIME`.
171    fn clock(&mut self) -> Result<Micros, &'static str> {
172        match self.word()?.split_once('=') {
173            Some(("at", value)) => Micros::parse_epoch(value).ok_or("bad at"),
174            _ => Err("no at= header"),
175        }
176    }
177
178    fn rest(self) -> Vec<String> {
179        self.0.collect()
180    }
181}
182
183/// Value of the first `key value` pair with this key — a convention clients may
184/// write their payload in, unrelated to the `key=value` headers the protocol
185/// puts in front of one.
186pub fn field<'a>(words: &'a [String], key: &str) -> Option<&'a str> {
187    words
188        .chunks_exact(2)
189        .find(|pair| pair[0] == key)
190        .map(|pair| pair[1].as_str())
191}
192
193/// What a blocked shell is told to run next: one command, as an arglist — the
194/// same shape a message has, encoded the same way.
195#[derive(Debug)]
196pub struct Answer(Vec<String>);
197
198impl Answer {
199    /// A command and the arguments it is given. The command word stands apart
200    /// because a command of no words is not one.
201    pub fn of(command: impl Into<String>, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
202        let mut words = vec![command.into()];
203        words.extend(args.into_iter().map(Into::into));
204
205        Self(words)
206    }
207
208    /// A status for the ask, leaving the frame that asked to carry on.
209    pub fn status(code: u8) -> Self {
210        Self::of("__bc_status", [code.to_string()])
211    }
212
213    /// A word this rig has no answer for: 127, bash's own "command not found".
214    pub fn unknown() -> Self {
215        Self::status(127)
216    }
217
218    /// `return code` in the frame that asked, so the function holding the call
219    /// site returns — where [`status`](Answer::status) only gives the ask a
220    /// status to test.
221    pub fn returning(code: u8) -> Self {
222        Self::of("return", [code.to_string()])
223    }
224}
225
226/// One line, whatever it carries: the bash array literal a shell reads back
227/// with `declare -a` — how an answer travels the reply pipe.
228impl fmt::Display for Answer {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        write!(f, "{}", emit_array(&self.0))
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    fn words(items: &[&str]) -> Vec<String> {
239        items.iter().map(|item| item.to_string()).collect()
240    }
241
242    fn line(kind: &str, payload: &[&str]) -> Line {
243        let mut all = words(&[kind, "at=1.000002"]);
244        all.extend(words(payload));
245
246        Line {
247            text: emit_array(&all),
248            heard_at: Micros(50),
249        }
250    }
251
252    fn spoke(payload: &[&str]) -> Message {
253        Message::read(line("SAY", payload)).expect("a message")
254    }
255
256    #[test]
257    fn the_protocols_words_come_off_and_the_clients_remain() {
258        let message = spoke(&["REC", "a space", ""]);
259
260        assert_eq!(message.verb, Verb::Say);
261        assert_eq!(message.stamp.sent_at, Micros(1_000_002));
262        assert_eq!(
263            message.stamp.heard_at,
264            Micros(50),
265            "the run's clock, from the read"
266        );
267        assert_eq!(
268            message.words,
269            words(&["REC", "a space", ""]),
270            "the payload alone"
271        );
272        assert_eq!(
273            message.behind("REC"),
274            Some(words(&["a space", ""]).as_slice())
275        );
276    }
277
278    #[test]
279    fn a_message_may_carry_nothing_of_its_own() {
280        let message = spoke(&[]);
281
282        assert!(message.words.is_empty());
283        assert_eq!(message.behind("REC"), None);
284    }
285
286    /// An account has no verb: the clock comes first.
287    #[test]
288    fn an_account_is_the_clock_and_the_pairs() {
289        let text = emit_array(&words(&[
290            "at=1.000002",
291            "zero",
292            "x.bash",
293            "flags",
294            "hB",
295        ]));
296        let account = Account::read(&text, Micros(50)).unwrap();
297
298        assert_eq!(
299            account.stamp,
300            Stamp {
301                sent_at: Micros(1_000_002),
302                heard_at: Micros(50)
303            }
304        );
305        assert_eq!(
306            field(&account.words, "zero"),
307            Some("x.bash")
308        );
309
310        let verbed = emit_array(&words(&["JOIN", "at=1.000002"]));
311        assert!(
312            Account::read(&verbed, Micros(0)).is_err(),
313            "a verb where the clock goes"
314        );
315    }
316
317    #[test]
318    fn a_header_the_protocol_did_not_write_is_an_error() {
319        let bad = [
320            emit_array(&words(&["MUMBLE", "at=1.000002"])),
321            emit_array(&words(&["JOIN", "at=1.000002"])),
322            emit_array(&words(&["SAY", "when=1.000002"])),
323            emit_array(&words(&["SAY", "at=1.0"])),
324            emit_array(&words(&["SAY"])),
325            "(unquoted".to_string(),
326        ];
327        for text in bad {
328            let refused = Message::read(Line {
329                text: text.clone(),
330                heard_at: Micros(0),
331            });
332            assert!(
333                refused.is_err(),
334                "{text} should not read"
335            );
336        }
337    }
338
339    /// The shell reads an answer with `read -r`, which stops at a newline, so a
340    /// word carrying one arrives escaped and the delimiter is the only newline
341    /// on that pipe.
342    #[test]
343    fn an_answer_is_one_line_whatever_it_carries() {
344        let carried = ["%s", "two\nlines", "a\ttab", "\u{ff}", "it's", ""];
345        let message = Answer::of("printf", carried).to_string();
346
347        assert!(
348            !message.contains('\n'),
349            "a raw newline would truncate the read: {message}"
350        );
351
352        let mut expected = vec!["printf".to_string()];
353        expected.extend(carried.iter().map(|word| word.to_string()));
354        assert_eq!(
355            parse_array(&message).unwrap(),
356            expected,
357            "and it still reads back"
358        );
359    }
360
361    /// A word may itself be a message, decoded one level at a time.
362    #[test]
363    fn messages_round_trip() {
364        let nested = emit_array(&words(&["INNER", "x y"]));
365        let message = spoke(&["TAG", "quote'inside", "two\nlines", &nested]);
366
367        assert_eq!(
368            message.words,
369            words(&["TAG", "quote'inside", "two\nlines", &nested])
370        );
371        assert_eq!(
372            parse_array(&nested).unwrap(),
373            words(&["INNER", "x y"])
374        );
375    }
376}