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}