Skip to main content

rustev_surface/
host.rs

1//! What a host supplies to a command (spec 027, 3.1): its inputs as bytes
2//! by label, bounded as the host's transport is, and a place for the
3//! artifacts a command produces. The CLI's labels are file paths and its
4//! host is the file system; a binding's are names and memory.
5
6/// Why an input could not be read or an output written: status
7/// `io_error`, exit 3.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct IoFail {
10    pub path: String,
11    pub detail: String,
12}
13
14/// A classified read failure: the input's own, or the command budget's.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ReadFail {
17    File(IoFail),
18    Budget(IoFail),
19}
20
21/// The failure of an input that does not exist where one is required.
22pub fn missing(label: &str) -> IoFail {
23    IoFail {
24        path: label.into(),
25        detail: "no such file".into(),
26    }
27}
28
29/// The host of one command.
30pub trait Host {
31    /// Read `label`, at most `max` bytes of it; `None` when it does not
32    /// exist. A failure of the input is told from an exhausted command
33    /// budget, which depends on read order rather than on the input.
34    fn read_classified(&self, label: &str, max: usize) -> Result<Option<Vec<u8>>, ReadFail>;
35
36    /// Refuse an output that already exists, before any work. A pure check
37    /// that changes nothing, so calling it again for the same label gives
38    /// the same answer: commands check every output up front and again
39    /// just before writing.
40    fn ensure_absent(&self, label: &str) -> Result<(), IoFail>;
41
42    /// Publish one output, never overwriting.
43    fn create(&self, label: &str, bytes: &[u8]) -> Result<(), IoFail>;
44
45    /// Publish every `(label, bytes)`, all or none.
46    fn write_all(&self, files: &[(String, Vec<u8>)]) -> Result<(), IoFail>;
47
48    /// The label of `name` inside the output or input directory `dir`.
49    fn join(&self, dir: &str, name: &str) -> String;
50
51    /// Read `label` up to its document limit plus one byte, so an oversized
52    /// input reaches the parser and is refused by the document's own bound.
53    fn read(&self, label: &str, limit: usize) -> Result<Vec<u8>, IoFail> {
54        self.read_bounded(label, limit.saturating_add(1))
55            .and_then(|r| r.ok_or_else(|| missing(label)))
56    }
57
58    /// Like [`Host::read`] with an explicit maximum, returning `None` for an
59    /// input that does not exist.
60    fn read_bounded(&self, label: &str, max: usize) -> Result<Option<Vec<u8>>, IoFail> {
61        self.read_classified(label, max).map_err(|e| match e {
62            ReadFail::File(f) | ReadFail::Budget(f) => f,
63        })
64    }
65}
66
67/// A host whose inputs are named byte strings and whose outputs stay in
68/// memory: what the bindings give the surface (spec 027, 3.1.3), where a
69/// command's artifacts are returned as bytes rather than written to files.
70/// It has no aggregate read budget; each read is bounded by its maximum.
71#[derive(Debug, Default)]
72pub struct Memory {
73    // Private, like the outputs: fixed when the host is made, so what a
74    // command reads cannot change between its checks and its reads.
75    inputs: std::collections::BTreeMap<String, Vec<u8>>,
76    // Private: only `create` and `write_all` borrow it, never across a
77    // call, so no caller can hold a borrow that makes them panic.
78    outputs: std::cell::RefCell<std::collections::BTreeMap<String, Vec<u8>>>,
79}
80
81impl Memory {
82    pub fn new(inputs: std::collections::BTreeMap<String, Vec<u8>>) -> Self {
83        Memory {
84            inputs,
85            outputs: Default::default(),
86        }
87    }
88
89    /// The inputs, by label.
90    pub fn inputs(&self) -> &std::collections::BTreeMap<String, Vec<u8>> {
91        &self.inputs
92    }
93
94    /// A copy of one published output, by label.
95    pub fn output(&self, label: &str) -> Option<Vec<u8>> {
96        self.outputs.borrow().get(label).cloned()
97    }
98
99    /// The outputs the command published, by label.
100    pub fn into_outputs(self) -> std::collections::BTreeMap<String, Vec<u8>> {
101        self.outputs.into_inner()
102    }
103}
104
105impl Host for Memory {
106    // At most `max` bytes, as the CLI's file host reads: a caller that must
107    // tell an oversized input asks for its limit plus one byte and compares
108    // lengths.
109    fn read_classified(&self, label: &str, max: usize) -> Result<Option<Vec<u8>>, ReadFail> {
110        Ok(self
111            .inputs
112            .get(label)
113            .map(|b| b[..b.len().min(max)].to_vec()))
114    }
115
116    fn ensure_absent(&self, label: &str) -> Result<(), IoFail> {
117        let detail = if self.outputs.borrow().contains_key(label) {
118            "already exists; outputs are never overwritten"
119        } else if self.inputs.contains_key(label) {
120            // As on a file system, where an input is a file at that path and
121            // the CLI's file host refuses to replace it: the same outcome on
122            // every surface.
123            "names an input; an output never replaces one"
124        } else {
125            return Ok(());
126        };
127        Err(IoFail {
128            path: label.into(),
129            detail: detail.into(),
130        })
131    }
132
133    fn create(&self, label: &str, bytes: &[u8]) -> Result<(), IoFail> {
134        self.ensure_absent(label)?;
135        self.outputs
136            .borrow_mut()
137            .insert(label.into(), bytes.to_vec());
138        Ok(())
139    }
140
141    fn write_all(&self, files: &[(String, Vec<u8>)]) -> Result<(), IoFail> {
142        // All or none: a failure removes what this call already wrote.
143        for (i, (l, b)) in files.iter().enumerate() {
144            if let Err(e) = self.create(l, b) {
145                let mut out = self.outputs.borrow_mut();
146                for (written, _) in &files[..i] {
147                    out.remove(written);
148                }
149                return Err(e);
150            }
151        }
152        Ok(())
153    }
154
155    fn join(&self, dir: &str, name: &str) -> String {
156        if dir.is_empty() {
157            name.to_string()
158        } else {
159            format!("{dir}/{name}")
160        }
161    }
162}