1use crate::{Error, Result};
19use bytes::Bytes;
20use std::collections::HashMap;
21use std::fmt::Debug;
22use std::path::PathBuf;
23use std::sync::Arc;
24
25#[derive(Clone)]
42pub struct Context {
43 fs: Arc<dyn FileRead>,
44 http: Arc<dyn HttpSend>,
45 env: Arc<dyn Env>,
46 cmd: Arc<dyn CommandExecute>,
47}
48
49impl Debug for Context {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("Context")
52 .field("fs", &self.fs)
53 .field("http", &self.http)
54 .field("env", &self.env)
55 .field("cmd", &self.cmd)
56 .finish()
57 }
58}
59
60impl Default for Context {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl Context {
67 pub fn new() -> Self {
83 Self {
84 fs: Arc::new(NoopFileRead),
85 http: Arc::new(NoopHttpSend),
86 env: Arc::new(NoopEnv),
87 cmd: Arc::new(NoopCommandExecute),
88 }
89 }
90
91 pub fn with_file_read(mut self, fs: impl FileRead) -> Self {
93 self.fs = Arc::new(fs);
94 self
95 }
96
97 pub fn with_http_send(mut self, http: impl HttpSend) -> Self {
99 self.http = Arc::new(http);
100 self
101 }
102
103 pub fn with_env(mut self, env: impl Env) -> Self {
105 self.env = Arc::new(env);
106 self
107 }
108
109 pub fn with_command_execute(mut self, cmd: impl CommandExecute) -> Self {
111 self.cmd = Arc::new(cmd);
112 self
113 }
114
115 #[inline]
117 pub async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
118 self.fs.file_read(path).await
119 }
120
121 pub async fn file_read_as_string(&self, path: &str) -> Result<String> {
123 let bytes = self.file_read(path).await?;
124 Ok(String::from_utf8_lossy(&bytes).to_string())
125 }
126
127 #[inline]
129 pub async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
130 self.http.http_send(req).await
131 }
132
133 pub async fn http_send_as_string(
135 &self,
136 req: http::Request<Bytes>,
137 ) -> Result<http::Response<String>> {
138 let (parts, body) = self.http.http_send(req).await?.into_parts();
139 let body = String::from_utf8_lossy(&body).to_string();
140 Ok(http::Response::from_parts(parts, body))
141 }
142
143 #[inline]
145 pub fn home_dir(&self) -> Option<PathBuf> {
146 self.env.home_dir()
147 }
148
149 pub fn expand_home_dir(&self, path: &str) -> Option<String> {
155 if !path.starts_with("~/") && !path.starts_with("~\\") {
156 Some(path.to_string())
157 } else {
158 self.home_dir()
159 .map(|home| path.replace('~', &home.to_string_lossy()))
160 }
161 }
162
163 #[inline]
168 pub fn env_var(&self, key: &str) -> Option<String> {
169 self.env.var(key)
170 }
171
172 #[inline]
175 pub fn env_vars(&self) -> HashMap<String, String> {
176 self.env.vars()
177 }
178
179 pub async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
183 self.cmd.command_execute(program, args).await
184 }
185}
186
187#[async_trait::async_trait]
191pub trait FileRead: Debug + Send + Sync + 'static {
192 async fn file_read(&self, path: &str) -> Result<Vec<u8>>;
194}
195
196#[async_trait::async_trait]
201pub trait HttpSend: Debug + Send + Sync + 'static {
202 async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>>;
204}
205
206pub trait Env: Debug + Send + Sync + 'static {
208 fn var(&self, key: &str) -> Option<String>;
213
214 fn vars(&self) -> HashMap<String, String>;
217
218 fn home_dir(&self) -> Option<PathBuf>;
220}
221
222#[derive(Debug, Copy, Clone)]
224pub struct OsEnv;
225
226impl Env for OsEnv {
227 fn var(&self, key: &str) -> Option<String> {
228 std::env::var_os(key)?.into_string().ok()
229 }
230
231 fn vars(&self) -> HashMap<String, String> {
232 std::env::vars().collect()
233 }
234
235 #[cfg(any(unix, target_os = "redox"))]
236 fn home_dir(&self) -> Option<PathBuf> {
237 #[allow(deprecated)]
238 std::env::home_dir()
239 }
240
241 #[cfg(windows)]
242 fn home_dir(&self) -> Option<PathBuf> {
243 windows::home_dir_inner()
244 }
245
246 #[cfg(target_arch = "wasm32")]
247 fn home_dir(&self) -> Option<PathBuf> {
248 None
249 }
250}
251
252#[derive(Debug, Clone, Default)]
256pub struct StaticEnv {
257 pub home_dir: Option<PathBuf>,
259 pub envs: HashMap<String, String>,
261}
262
263impl Env for StaticEnv {
264 fn var(&self, key: &str) -> Option<String> {
265 self.envs.get(key).cloned()
266 }
267
268 fn vars(&self) -> HashMap<String, String> {
269 self.envs.clone()
270 }
271
272 fn home_dir(&self) -> Option<PathBuf> {
273 self.home_dir.clone()
274 }
275}
276
277#[derive(Debug, Clone)]
279pub struct CommandOutput {
280 pub status: i32,
282 pub stdout: Vec<u8>,
284 pub stderr: Vec<u8>,
286}
287
288impl CommandOutput {
289 pub fn success(&self) -> bool {
291 self.status == 0
292 }
293}
294
295#[async_trait::async_trait]
303pub trait CommandExecute: Debug + Send + Sync + 'static {
304 async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput>;
306}
307
308#[derive(Debug, Clone, Copy, Default)]
312pub struct NoopFileRead;
313
314#[async_trait::async_trait]
315impl FileRead for NoopFileRead {
316 async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
317 Err(Error::unexpected(
318 "file reading not supported: no file reader configured",
319 ))
320 }
321}
322
323#[derive(Debug, Clone, Copy, Default)]
327pub struct NoopHttpSend;
328
329#[async_trait::async_trait]
330impl HttpSend for NoopHttpSend {
331 async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
332 Err(Error::unexpected(
333 "HTTP sending not supported: no HTTP client configured",
334 ))
335 }
336}
337
338#[derive(Debug, Clone, Copy, Default)]
342pub struct NoopEnv;
343
344impl Env for NoopEnv {
345 fn var(&self, _key: &str) -> Option<String> {
346 None
347 }
348
349 fn vars(&self) -> HashMap<String, String> {
350 HashMap::new()
351 }
352
353 fn home_dir(&self) -> Option<PathBuf> {
354 None
355 }
356}
357
358#[derive(Debug, Clone, Copy, Default)]
362pub struct NoopCommandExecute;
363
364#[async_trait::async_trait]
365impl CommandExecute for NoopCommandExecute {
366 async fn command_execute(&self, _program: &str, _args: &[&str]) -> Result<CommandOutput> {
367 Err(Error::unexpected(
368 "command execution not supported: no command executor configured",
369 ))
370 }
371}
372
373#[cfg(target_os = "windows")]
374mod windows {
375 use std::env;
376 use std::ffi::OsString;
377 use std::os::windows::ffi::OsStringExt;
378 use std::path::PathBuf;
379 use std::ptr;
380 use std::slice;
381
382 use windows_sys::Win32::Foundation::S_OK;
383 use windows_sys::Win32::System::Com::CoTaskMemFree;
384 use windows_sys::Win32::UI::Shell::{
385 FOLDERID_Profile, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
386 };
387
388 pub fn home_dir_inner() -> Option<PathBuf> {
389 env::var_os("USERPROFILE")
390 .filter(|s| !s.is_empty())
391 .map(PathBuf::from)
392 .or_else(home_dir_crt)
393 }
394
395 #[cfg(not(target_vendor = "uwp"))]
396 fn home_dir_crt() -> Option<PathBuf> {
397 unsafe {
398 let mut path = ptr::null_mut();
399 match SHGetKnownFolderPath(
400 &FOLDERID_Profile,
401 KF_FLAG_DONT_VERIFY as u32,
402 std::ptr::null_mut(),
403 &mut path,
404 ) {
405 S_OK => {
406 let path_slice = slice::from_raw_parts(path, wcslen(path));
407 let s = OsString::from_wide(&path_slice);
408 CoTaskMemFree(path.cast());
409 Some(PathBuf::from(s))
410 }
411 _ => {
412 CoTaskMemFree(path.cast());
414 None
415 }
416 }
417 }
418 }
419
420 #[cfg(target_vendor = "uwp")]
421 fn home_dir_crt() -> Option<PathBuf> {
422 None
423 }
424
425 unsafe extern "C" {
426 unsafe fn wcslen(buf: *const u16) -> usize;
427 }
428
429 #[cfg(not(target_vendor = "uwp"))]
430 #[cfg(test)]
431 mod tests {
432 use super::home_dir_inner;
433 use std::env;
434 use std::ops::Deref;
435 use std::path::{Path, PathBuf};
436
437 #[test]
438 fn test_with_without() {
439 let olduserprofile = env::var_os("USERPROFILE").unwrap();
440 unsafe {
441 env::remove_var("HOME");
442 env::remove_var("USERPROFILE");
443 }
444 assert_eq!(home_dir_inner(), Some(PathBuf::from(olduserprofile)));
445
446 let home = Path::new(r"C:\Users\foo tar baz");
447 unsafe {
448 env::set_var("HOME", home.as_os_str());
449 env::set_var("USERPROFILE", home.as_os_str());
450 }
451 assert_ne!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
452 assert_eq!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
453 }
454 }
455}