1use std::io::{self, IsTerminal, Read, Write};
10use std::path::Path;
11
12pub trait Host {
14 fn env(&self, key: &str) -> Option<String>;
19
20 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>>;
26
27 fn file_mode(&self, path: &Path) -> io::Result<Option<u32>>;
33
34 fn write_new_directory(
44 &mut self,
45 _path: &Path,
46 _files: &[(String, Vec<u8>)],
47 ) -> io::Result<()> {
48 Err(io::Error::new(
49 io::ErrorKind::Unsupported,
50 "this host does not support diagnostics bundle output",
51 ))
52 }
53
54 fn write_stdout(&mut self, bytes: &[u8]) -> io::Result<()>;
60
61 fn flush_stdout(&mut self) -> io::Result<()>;
67
68 fn write_stderr(&mut self, bytes: &[u8]);
73
74 fn is_stdin_interactive(&self) -> bool;
76
77 fn is_stdout_terminal(&self) -> bool;
79
80 fn read_confirmation(&mut self) -> io::Result<Option<String>>;
89
90 fn new_operation_id(&mut self) -> String;
95}
96
97#[derive(Debug)]
99pub struct ProcessHost {
100 stdout: io::Stdout,
101 stderr: io::Stderr,
102 counter: u64,
103}
104
105impl ProcessHost {
106 #[must_use]
108 pub fn new() -> Self {
109 Self {
110 stdout: io::stdout(),
111 stderr: io::stderr(),
112 counter: 0,
113 }
114 }
115}
116
117impl Default for ProcessHost {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123impl Host for ProcessHost {
124 fn env(&self, key: &str) -> Option<String> {
125 std::env::var(key).ok().filter(|value| !value.is_empty())
126 }
127
128 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
129 std::fs::read(path)
130 }
131
132 fn file_mode(&self, path: &Path) -> io::Result<Option<u32>> {
133 let metadata = std::fs::metadata(path)?;
134 #[cfg(unix)]
135 {
136 use std::os::unix::fs::PermissionsExt;
137 Ok(Some(metadata.permissions().mode()))
138 }
139 #[cfg(not(unix))]
140 {
141 let _ = metadata;
142 Ok(None)
143 }
144 }
145
146 fn write_new_directory(&mut self, path: &Path, files: &[(String, Vec<u8>)]) -> io::Result<()> {
147 if path.try_exists()? {
148 return Err(io::Error::new(
149 io::ErrorKind::AlreadyExists,
150 "the bundle target already exists",
151 ));
152 }
153 let mut temporary = path.as_os_str().to_owned();
154 temporary.push(format!(".tmp-{}", std::process::id()));
155 let temporary = std::path::PathBuf::from(temporary);
156 if temporary.try_exists()? {
157 return Err(io::Error::new(
158 io::ErrorKind::AlreadyExists,
159 "the bundle temporary target already exists",
160 ));
161 }
162 std::fs::create_dir(&temporary)?;
163 let written = files.iter().try_for_each(|(name, bytes)| {
164 if name.is_empty()
165 || name.contains('/')
166 || name.contains('\\')
167 || matches!(name.as_str(), "." | "..")
168 {
169 return Err(io::Error::new(
170 io::ErrorKind::InvalidInput,
171 "the bundle file name is not accepted",
172 ));
173 }
174 let target = temporary.join(name);
175 let mut file = std::fs::OpenOptions::new()
176 .write(true)
177 .create_new(true)
178 .open(target)?;
179 file.write_all(bytes)?;
180 file.sync_all()
181 });
182 if let Err(error) = written {
183 let _ = std::fs::remove_dir_all(&temporary);
184 return Err(error);
185 }
186 if let Err(error) = std::fs::create_dir(path) {
187 let _ = std::fs::remove_dir_all(&temporary);
188 return Err(error);
189 }
190 let installed = files
191 .iter()
192 .try_for_each(|(name, _)| std::fs::rename(temporary.join(name), path.join(name)));
193 if let Err(error) = installed {
194 let _ = std::fs::remove_dir_all(path);
195 let _ = std::fs::remove_dir_all(&temporary);
196 return Err(error);
197 }
198 std::fs::remove_dir(&temporary)?;
199 Ok(())
200 }
201
202 fn write_stdout(&mut self, bytes: &[u8]) -> io::Result<()> {
203 self.stdout.write_all(bytes)
204 }
205
206 fn flush_stdout(&mut self) -> io::Result<()> {
207 self.stdout.flush()
208 }
209
210 fn write_stderr(&mut self, bytes: &[u8]) {
211 let _ = self.stderr.write_all(bytes);
212 let _ = self.stderr.flush();
213 }
214
215 fn is_stdin_interactive(&self) -> bool {
216 io::stdin().is_terminal()
217 }
218
219 fn is_stdout_terminal(&self) -> bool {
220 self.stdout.is_terminal()
221 }
222
223 fn read_confirmation(&mut self) -> io::Result<Option<String>> {
224 let mut buffer = String::new();
225 let mut handle = io::stdin().lock();
226 let mut byte = [0_u8; 1];
227 loop {
228 match handle.read(&mut byte)? {
229 0 => break,
230 _ if byte[0] == b'\n' => break,
231 _ => buffer.push(char::from(byte[0])),
232 }
233 if buffer.len() > MAX_CONFIRMATION_BYTES {
234 break;
235 }
236 }
237 if buffer.is_empty() {
238 return Ok(None);
239 }
240 Ok(Some(buffer))
241 }
242
243 fn new_operation_id(&mut self) -> String {
244 self.counter += 1;
248 let nanos = std::time::SystemTime::now()
249 .duration_since(std::time::UNIX_EPOCH)
250 .map_or(0, |value| value.as_nanos());
251 format!("cli-{}-{nanos}-{}", std::process::id(), self.counter)
252 }
253}
254
255const MAX_CONFIRMATION_BYTES: usize = 64;
257
258#[cfg(test)]
259pub(crate) mod testing {
260 use std::collections::BTreeMap;
261 use std::io;
262 use std::path::{Path, PathBuf};
263
264 use super::Host;
265
266 #[derive(Debug, Default)]
268 pub(crate) struct TestHost {
269 pub(crate) env: BTreeMap<String, String>,
270 pub(crate) files: BTreeMap<PathBuf, Vec<u8>>,
271 pub(crate) modes: BTreeMap<PathBuf, u32>,
272 pub(crate) directories: BTreeMap<PathBuf, Vec<String>>,
273 pub(crate) stdout: Vec<u8>,
274 pub(crate) stderr: Vec<u8>,
275 pub(crate) stdin_interactive: bool,
276 pub(crate) stdout_terminal: bool,
277 pub(crate) confirmation: Option<String>,
278 pub(crate) stdout_capacity: Option<usize>,
280 pub(crate) operation_ids: u64,
281 }
282
283 impl TestHost {
284 pub(crate) fn new() -> Self {
285 Self::default()
286 }
287
288 pub(crate) fn with_env(mut self, key: &str, value: &str) -> Self {
289 self.env.insert(key.to_owned(), value.to_owned());
290 self
291 }
292
293 pub(crate) fn with_file(mut self, path: &str, contents: &str) -> Self {
294 self.files
295 .insert(PathBuf::from(path), contents.as_bytes().to_vec());
296 self.modes.insert(PathBuf::from(path), 0o600);
297 self
298 }
299
300 pub(crate) fn with_mode(mut self, path: &str, mode: u32) -> Self {
301 self.modes.insert(PathBuf::from(path), mode);
302 self
303 }
304
305 pub(crate) fn with_stdout_capacity(mut self, bytes: usize) -> Self {
306 self.stdout_capacity = Some(bytes);
307 self
308 }
309
310 pub(crate) fn stdout_text(&self) -> String {
311 String::from_utf8_lossy(&self.stdout).into_owned()
312 }
313 }
314
315 impl Host for TestHost {
316 fn env(&self, key: &str) -> Option<String> {
317 self.env.get(key).cloned().filter(|value| !value.is_empty())
318 }
319
320 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
321 self.files.get(path).cloned().ok_or_else(|| {
322 io::Error::new(io::ErrorKind::NotFound, "the test host has no such file")
323 })
324 }
325
326 fn file_mode(&self, path: &Path) -> io::Result<Option<u32>> {
327 if !self.files.contains_key(path) {
328 return Err(io::Error::new(
329 io::ErrorKind::NotFound,
330 "the test host has no such file",
331 ));
332 }
333 Ok(self.modes.get(path).copied())
334 }
335
336 fn write_new_directory(
337 &mut self,
338 path: &Path,
339 files: &[(String, Vec<u8>)],
340 ) -> io::Result<()> {
341 if self.directories.contains_key(path) {
342 return Err(io::Error::new(
343 io::ErrorKind::AlreadyExists,
344 "target exists",
345 ));
346 }
347 self.directories.insert(
348 path.to_path_buf(),
349 files.iter().map(|(name, _)| name.clone()).collect(),
350 );
351 for (name, bytes) in files {
352 self.files.insert(path.join(name), bytes.clone());
353 }
354 Ok(())
355 }
356
357 fn write_stdout(&mut self, bytes: &[u8]) -> io::Result<()> {
358 if let Some(capacity) = self.stdout_capacity
359 && self.stdout.len() + bytes.len() > capacity
360 {
361 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "closed pipe"));
362 }
363 self.stdout.extend_from_slice(bytes);
364 Ok(())
365 }
366
367 fn flush_stdout(&mut self) -> io::Result<()> {
368 Ok(())
369 }
370
371 fn write_stderr(&mut self, bytes: &[u8]) {
372 self.stderr.extend_from_slice(bytes);
373 }
374
375 fn is_stdin_interactive(&self) -> bool {
376 self.stdin_interactive
377 }
378
379 fn is_stdout_terminal(&self) -> bool {
380 self.stdout_terminal
381 }
382
383 fn read_confirmation(&mut self) -> io::Result<Option<String>> {
384 Ok(self.confirmation.take())
385 }
386
387 fn new_operation_id(&mut self) -> String {
388 self.operation_ids += 1;
389 format!("test-operation-{}", self.operation_ids)
390 }
391 }
392}