1use crate::{Error, Result};
2use bytes::Bytes;
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::path::PathBuf;
6use std::sync::Arc;
7
8#[derive(Clone)]
25pub struct Context {
26 fs: Arc<dyn FileRead>,
27 http: Arc<dyn HttpSend>,
28 env: Arc<dyn Env>,
29 cmd: Arc<dyn CommandExecute>,
30}
31
32impl Debug for Context {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("Context")
35 .field("fs", &self.fs)
36 .field("http", &self.http)
37 .field("env", &self.env)
38 .field("cmd", &self.cmd)
39 .finish()
40 }
41}
42
43impl Default for Context {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl Context {
50 pub fn new() -> Self {
66 Self {
67 fs: Arc::new(NoopFileRead),
68 http: Arc::new(NoopHttpSend),
69 env: Arc::new(NoopEnv),
70 cmd: Arc::new(NoopCommandExecute),
71 }
72 }
73
74 pub fn with_file_read(mut self, fs: impl FileRead) -> Self {
76 self.fs = Arc::new(fs);
77 self
78 }
79
80 pub fn with_http_send(mut self, http: impl HttpSend) -> Self {
82 self.http = Arc::new(http);
83 self
84 }
85
86 pub fn with_env(mut self, env: impl Env) -> Self {
88 self.env = Arc::new(env);
89 self
90 }
91
92 pub fn with_command_execute(mut self, cmd: impl CommandExecute) -> Self {
94 self.cmd = Arc::new(cmd);
95 self
96 }
97
98 #[inline]
100 pub async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
101 self.fs.file_read(path).await
102 }
103
104 pub async fn file_read_as_string(&self, path: &str) -> Result<String> {
106 let bytes = self.file_read(path).await?;
107 Ok(String::from_utf8_lossy(&bytes).to_string())
108 }
109
110 #[inline]
112 pub async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
113 self.http.http_send(req).await
114 }
115
116 pub async fn http_send_as_string(
118 &self,
119 req: http::Request<Bytes>,
120 ) -> Result<http::Response<String>> {
121 let (parts, body) = self.http.http_send(req).await?.into_parts();
122 let body = String::from_utf8_lossy(&body).to_string();
123 Ok(http::Response::from_parts(parts, body))
124 }
125
126 #[inline]
128 pub fn home_dir(&self) -> Option<PathBuf> {
129 self.env.home_dir()
130 }
131
132 pub fn expand_home_dir(&self, path: &str) -> Option<String> {
138 if !path.starts_with("~/") && !path.starts_with("~\\") {
139 Some(path.to_string())
140 } else {
141 self.home_dir()
142 .map(|home| path.replace('~', &home.to_string_lossy()))
143 }
144 }
145
146 #[inline]
151 pub fn env_var(&self, key: &str) -> Option<String> {
152 self.env.var(key)
153 }
154
155 #[inline]
158 pub fn env_vars(&self) -> HashMap<String, String> {
159 self.env.vars()
160 }
161
162 pub async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
166 self.cmd.command_execute(program, args).await
167 }
168}
169
170#[async_trait::async_trait]
174pub trait FileRead: Debug + Send + Sync + 'static {
175 async fn file_read(&self, path: &str) -> Result<Vec<u8>>;
177}
178
179#[async_trait::async_trait]
184pub trait HttpSend: Debug + Send + Sync + 'static {
185 async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>>;
187}
188
189pub trait Env: Debug + Send + Sync + 'static {
191 fn var(&self, key: &str) -> Option<String>;
196
197 fn vars(&self) -> HashMap<String, String>;
200
201 fn home_dir(&self) -> Option<PathBuf>;
203}
204
205#[derive(Debug, Copy, Clone)]
207pub struct OsEnv;
208
209impl Env for OsEnv {
210 fn var(&self, key: &str) -> Option<String> {
211 std::env::var_os(key)?.into_string().ok()
212 }
213
214 fn vars(&self) -> HashMap<String, String> {
215 std::env::vars().collect()
216 }
217
218 #[cfg(any(unix, target_os = "redox"))]
219 fn home_dir(&self) -> Option<PathBuf> {
220 #[allow(deprecated)]
221 std::env::home_dir()
222 }
223
224 #[cfg(windows)]
225 fn home_dir(&self) -> Option<PathBuf> {
226 windows::home_dir_inner()
227 }
228
229 #[cfg(target_arch = "wasm32")]
230 fn home_dir(&self) -> Option<PathBuf> {
231 None
232 }
233}
234
235#[derive(Debug, Clone, Default)]
239pub struct StaticEnv {
240 pub home_dir: Option<PathBuf>,
242 pub envs: HashMap<String, String>,
244}
245
246impl Env for StaticEnv {
247 fn var(&self, key: &str) -> Option<String> {
248 self.envs.get(key).cloned()
249 }
250
251 fn vars(&self) -> HashMap<String, String> {
252 self.envs.clone()
253 }
254
255 fn home_dir(&self) -> Option<PathBuf> {
256 self.home_dir.clone()
257 }
258}
259
260#[derive(Debug, Clone)]
262pub struct CommandOutput {
263 pub status: i32,
265 pub stdout: Vec<u8>,
267 pub stderr: Vec<u8>,
269}
270
271impl CommandOutput {
272 pub fn success(&self) -> bool {
274 self.status == 0
275 }
276}
277
278#[async_trait::async_trait]
286pub trait CommandExecute: Debug + Send + Sync + 'static {
287 async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput>;
289}
290
291#[derive(Debug, Clone, Copy, Default)]
295pub struct NoopFileRead;
296
297#[async_trait::async_trait]
298impl FileRead for NoopFileRead {
299 async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
300 Err(Error::unexpected(
301 "file reading not supported: no file reader configured",
302 ))
303 }
304}
305
306#[derive(Debug, Clone, Copy, Default)]
310pub struct NoopHttpSend;
311
312#[async_trait::async_trait]
313impl HttpSend for NoopHttpSend {
314 async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
315 Err(Error::unexpected(
316 "HTTP sending not supported: no HTTP client configured",
317 ))
318 }
319}
320
321#[derive(Debug, Clone, Copy, Default)]
325pub struct NoopEnv;
326
327impl Env for NoopEnv {
328 fn var(&self, _key: &str) -> Option<String> {
329 None
330 }
331
332 fn vars(&self) -> HashMap<String, String> {
333 HashMap::new()
334 }
335
336 fn home_dir(&self) -> Option<PathBuf> {
337 None
338 }
339}
340
341#[derive(Debug, Clone, Copy, Default)]
345pub struct NoopCommandExecute;
346
347#[async_trait::async_trait]
348impl CommandExecute for NoopCommandExecute {
349 async fn command_execute(&self, _program: &str, _args: &[&str]) -> Result<CommandOutput> {
350 Err(Error::unexpected(
351 "command execution not supported: no command executor configured",
352 ))
353 }
354}
355
356#[cfg(target_os = "windows")]
357mod windows {
358 use std::env;
359 use std::ffi::OsString;
360 use std::os::windows::ffi::OsStringExt;
361 use std::path::PathBuf;
362 use std::ptr;
363 use std::slice;
364
365 use windows_sys::Win32::Foundation::S_OK;
366 use windows_sys::Win32::System::Com::CoTaskMemFree;
367 use windows_sys::Win32::UI::Shell::{
368 FOLDERID_Profile, SHGetKnownFolderPath, KF_FLAG_DONT_VERIFY,
369 };
370
371 pub fn home_dir_inner() -> Option<PathBuf> {
372 env::var_os("USERPROFILE")
373 .filter(|s| !s.is_empty())
374 .map(PathBuf::from)
375 .or_else(home_dir_crt)
376 }
377
378 #[cfg(not(target_vendor = "uwp"))]
379 fn home_dir_crt() -> Option<PathBuf> {
380 unsafe {
381 let mut path = ptr::null_mut();
382 match SHGetKnownFolderPath(
383 &FOLDERID_Profile,
384 KF_FLAG_DONT_VERIFY as u32,
385 std::ptr::null_mut(),
386 &mut path,
387 ) {
388 S_OK => {
389 let path_slice = slice::from_raw_parts(path, wcslen(path));
390 let s = OsString::from_wide(&path_slice);
391 CoTaskMemFree(path.cast());
392 Some(PathBuf::from(s))
393 }
394 _ => {
395 CoTaskMemFree(path.cast());
397 None
398 }
399 }
400 }
401 }
402
403 #[cfg(target_vendor = "uwp")]
404 fn home_dir_crt() -> Option<PathBuf> {
405 None
406 }
407
408 extern "C" {
409 fn wcslen(buf: *const u16) -> usize;
410 }
411
412 #[cfg(not(target_vendor = "uwp"))]
413 #[cfg(test)]
414 mod tests {
415 use super::home_dir_inner;
416 use std::env;
417 use std::ops::Deref;
418 use std::path::{Path, PathBuf};
419
420 #[test]
421 fn test_with_without() {
422 let olduserprofile = env::var_os("USERPROFILE").unwrap();
423
424 env::remove_var("HOME");
425 env::remove_var("USERPROFILE");
426
427 assert_eq!(home_dir_inner(), Some(PathBuf::from(olduserprofile)));
428
429 let home = Path::new(r"C:\Users\foo tar baz");
430
431 env::set_var("HOME", home.as_os_str());
432 assert_ne!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
433
434 env::set_var("USERPROFILE", home.as_os_str());
435 assert_eq!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
436 }
437 }
438}