1use crate::{BoxedFuture, Error, MaybeSend, Result};
19use bytes::Bytes;
20use std::collections::HashMap;
21use std::fmt::Debug;
22use std::future::Future;
23use std::ops::Deref;
24use std::path::PathBuf;
25use std::sync::Arc;
26
27#[derive(Clone)]
44pub struct Context {
45 fs: Arc<dyn FileReadDyn>,
46 http: Arc<dyn HttpSendDyn>,
47 env: Arc<dyn Env>,
48 cmd: Arc<dyn CommandExecuteDyn>,
49}
50
51impl Debug for Context {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("Context")
54 .field("fs", &self.fs)
55 .field("http", &self.http)
56 .field("env", &self.env)
57 .field("cmd", &self.cmd)
58 .finish()
59 }
60}
61
62impl Default for Context {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl Context {
69 pub fn new() -> Self {
85 Self {
86 fs: Arc::new(NoopFileRead),
87 http: Arc::new(NoopHttpSend),
88 env: Arc::new(NoopEnv),
89 cmd: Arc::new(NoopCommandExecute),
90 }
91 }
92
93 pub fn with_file_read(mut self, fs: impl FileRead) -> Self {
95 self.fs = Arc::new(fs);
96 self
97 }
98
99 pub fn with_http_send(mut self, http: impl HttpSend) -> Self {
101 self.http = Arc::new(http);
102 self
103 }
104
105 pub fn with_env(mut self, env: impl Env) -> Self {
107 self.env = Arc::new(env);
108 self
109 }
110
111 pub fn with_command_execute(mut self, cmd: impl CommandExecute) -> Self {
113 self.cmd = Arc::new(cmd);
114 self
115 }
116
117 #[inline]
119 pub async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
120 self.fs.file_read_dyn(path).await
121 }
122
123 pub async fn file_read_as_string(&self, path: &str) -> Result<String> {
125 let bytes = self.file_read(path).await?;
126 Ok(String::from_utf8_lossy(&bytes).to_string())
127 }
128
129 #[inline]
131 pub async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
132 self.http.http_send_dyn(req).await
133 }
134
135 pub async fn http_send_as_string(
137 &self,
138 req: http::Request<Bytes>,
139 ) -> Result<http::Response<String>> {
140 let (parts, body) = self.http.http_send_dyn(req).await?.into_parts();
141 let body = String::from_utf8_lossy(&body).to_string();
142 Ok(http::Response::from_parts(parts, body))
143 }
144
145 #[inline]
147 pub fn home_dir(&self) -> Option<PathBuf> {
148 self.env.home_dir()
149 }
150
151 pub fn expand_home_dir(&self, path: &str) -> Option<String> {
157 if !path.starts_with("~/") && !path.starts_with("~\\") {
158 Some(path.to_string())
159 } else {
160 self.home_dir()
161 .map(|home| path.replace('~', &home.to_string_lossy()))
162 }
163 }
164
165 #[inline]
170 pub fn env_var(&self, key: &str) -> Option<String> {
171 self.env.var(key)
172 }
173
174 #[inline]
177 pub fn env_vars(&self) -> HashMap<String, String> {
178 self.env.vars()
179 }
180
181 pub async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
185 self.cmd.command_execute_dyn(program, args).await
186 }
187}
188
189pub trait FileRead: Debug + Send + Sync + 'static {
193 fn file_read(&self, path: &str) -> impl Future<Output = Result<Vec<u8>>> + MaybeSend;
195}
196
197pub trait FileReadDyn: Debug + Send + Sync + 'static {
199 fn file_read_dyn<'a>(&'a self, path: &'a str) -> BoxedFuture<'a, Result<Vec<u8>>>;
201}
202
203impl<T: FileRead + ?Sized> FileReadDyn for T {
204 fn file_read_dyn<'a>(&'a self, path: &'a str) -> BoxedFuture<'a, Result<Vec<u8>>> {
205 Box::pin(self.file_read(path))
206 }
207}
208
209impl<T: FileReadDyn + ?Sized> FileRead for Arc<T> {
210 async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
211 self.deref().file_read_dyn(path).await
212 }
213}
214
215pub trait HttpSend: Debug + Send + Sync + 'static {
224 fn http_send(
226 &self,
227 req: http::Request<Bytes>,
228 ) -> impl Future<Output = Result<http::Response<Bytes>>> + MaybeSend;
229}
230
231pub trait HttpSendDyn: Debug + Send + Sync + 'static {
233 fn http_send_dyn(
235 &self,
236 req: http::Request<Bytes>,
237 ) -> BoxedFuture<'_, Result<http::Response<Bytes>>>;
238}
239
240impl<T: HttpSend + ?Sized> HttpSendDyn for T {
241 fn http_send_dyn(
242 &self,
243 req: http::Request<Bytes>,
244 ) -> BoxedFuture<'_, Result<http::Response<Bytes>>> {
245 Box::pin(self.http_send(req))
246 }
247}
248
249impl<T: HttpSendDyn + ?Sized> HttpSend for Arc<T> {
250 async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
251 self.deref().http_send_dyn(req).await
252 }
253}
254
255pub trait Env: Debug + Send + Sync + 'static {
257 fn var(&self, key: &str) -> Option<String>;
262
263 fn vars(&self) -> HashMap<String, String>;
266
267 fn home_dir(&self) -> Option<PathBuf>;
269}
270
271#[derive(Debug, Copy, Clone)]
273pub struct OsEnv;
274
275impl Env for OsEnv {
276 fn var(&self, key: &str) -> Option<String> {
277 std::env::var_os(key)?.into_string().ok()
278 }
279
280 fn vars(&self) -> HashMap<String, String> {
281 std::env::vars().collect()
282 }
283
284 #[cfg(any(unix, target_os = "redox"))]
285 fn home_dir(&self) -> Option<PathBuf> {
286 #[allow(deprecated)]
287 std::env::home_dir()
288 }
289
290 #[cfg(windows)]
291 fn home_dir(&self) -> Option<PathBuf> {
292 windows::home_dir_inner()
293 }
294
295 #[cfg(target_arch = "wasm32")]
296 fn home_dir(&self) -> Option<PathBuf> {
297 None
298 }
299}
300
301#[derive(Debug, Clone, Default)]
305pub struct StaticEnv {
306 pub home_dir: Option<PathBuf>,
308 pub envs: HashMap<String, String>,
310}
311
312impl Env for StaticEnv {
313 fn var(&self, key: &str) -> Option<String> {
314 self.envs.get(key).cloned()
315 }
316
317 fn vars(&self) -> HashMap<String, String> {
318 self.envs.clone()
319 }
320
321 fn home_dir(&self) -> Option<PathBuf> {
322 self.home_dir.clone()
323 }
324}
325
326#[derive(Debug, Clone)]
328pub struct CommandOutput {
329 pub status: i32,
331 pub stdout: Vec<u8>,
333 pub stderr: Vec<u8>,
335}
336
337impl CommandOutput {
338 pub fn success(&self) -> bool {
340 self.status == 0
341 }
342}
343
344pub trait CommandExecute: Debug + Send + Sync + 'static {
352 fn command_execute<'a>(
354 &'a self,
355 program: &'a str,
356 args: &'a [&'a str],
357 ) -> impl Future<Output = Result<CommandOutput>> + MaybeSend + 'a;
358}
359
360pub trait CommandExecuteDyn: Debug + Send + Sync + 'static {
362 fn command_execute_dyn<'a>(
364 &'a self,
365 program: &'a str,
366 args: &'a [&'a str],
367 ) -> BoxedFuture<'a, Result<CommandOutput>>;
368}
369
370impl<T: CommandExecute + ?Sized> CommandExecuteDyn for T {
371 fn command_execute_dyn<'a>(
372 &'a self,
373 program: &'a str,
374 args: &'a [&'a str],
375 ) -> BoxedFuture<'a, Result<CommandOutput>> {
376 Box::pin(self.command_execute(program, args))
377 }
378}
379
380impl<T: CommandExecuteDyn + ?Sized> CommandExecute for Arc<T> {
381 async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
382 self.deref().command_execute_dyn(program, args).await
383 }
384}
385
386#[derive(Debug, Clone, Copy, Default)]
390pub struct NoopFileRead;
391
392impl FileRead for NoopFileRead {
393 async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
394 Err(Error::unexpected(
395 "file reading not supported: no file reader configured",
396 ))
397 }
398}
399
400#[derive(Debug, Clone, Copy, Default)]
404pub struct NoopHttpSend;
405
406impl HttpSend for NoopHttpSend {
407 async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
408 Err(Error::unexpected(
409 "HTTP sending not supported: no HTTP client configured",
410 ))
411 }
412}
413
414#[derive(Debug, Clone, Copy, Default)]
418pub struct NoopEnv;
419
420impl Env for NoopEnv {
421 fn var(&self, _key: &str) -> Option<String> {
422 None
423 }
424
425 fn vars(&self) -> HashMap<String, String> {
426 HashMap::new()
427 }
428
429 fn home_dir(&self) -> Option<PathBuf> {
430 None
431 }
432}
433
434#[derive(Debug, Clone, Copy, Default)]
438pub struct NoopCommandExecute;
439
440impl CommandExecute for NoopCommandExecute {
441 async fn command_execute(&self, _program: &str, _args: &[&str]) -> Result<CommandOutput> {
442 Err(Error::unexpected(
443 "command execution not supported: no command executor configured",
444 ))
445 }
446}
447
448#[cfg(target_os = "windows")]
449mod windows {
450 use std::env;
451 use std::ffi::OsString;
452 use std::os::windows::ffi::OsStringExt;
453 use std::path::PathBuf;
454 use std::ptr;
455 use std::slice;
456
457 use windows_sys::Win32::Foundation::S_OK;
458 use windows_sys::Win32::System::Com::CoTaskMemFree;
459 use windows_sys::Win32::UI::Shell::{
460 FOLDERID_Profile, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
461 };
462
463 pub fn home_dir_inner() -> Option<PathBuf> {
464 env::var_os("USERPROFILE")
465 .filter(|s| !s.is_empty())
466 .map(PathBuf::from)
467 .or_else(home_dir_crt)
468 }
469
470 #[cfg(not(target_vendor = "uwp"))]
471 fn home_dir_crt() -> Option<PathBuf> {
472 unsafe {
473 let mut path = ptr::null_mut();
474 match SHGetKnownFolderPath(
475 &FOLDERID_Profile,
476 KF_FLAG_DONT_VERIFY as u32,
477 std::ptr::null_mut(),
478 &mut path,
479 ) {
480 S_OK => {
481 let path_slice = slice::from_raw_parts(path, wcslen(path));
482 let s = OsString::from_wide(&path_slice);
483 CoTaskMemFree(path.cast());
484 Some(PathBuf::from(s))
485 }
486 _ => {
487 CoTaskMemFree(path.cast());
489 None
490 }
491 }
492 }
493 }
494
495 #[cfg(target_vendor = "uwp")]
496 fn home_dir_crt() -> Option<PathBuf> {
497 None
498 }
499
500 unsafe extern "C" {
501 unsafe fn wcslen(buf: *const u16) -> usize;
502 }
503
504 #[cfg(not(target_vendor = "uwp"))]
505 #[cfg(test)]
506 mod tests {
507 use super::home_dir_inner;
508 use std::env;
509 use std::ops::Deref;
510 use std::path::{Path, PathBuf};
511
512 #[test]
513 fn test_with_without() {
514 let olduserprofile = env::var_os("USERPROFILE").unwrap();
515 unsafe {
516 env::remove_var("HOME");
517 env::remove_var("USERPROFILE");
518 }
519 assert_eq!(home_dir_inner(), Some(PathBuf::from(olduserprofile)));
520
521 let home = Path::new(r"C:\Users\foo tar baz");
522 unsafe {
523 env::set_var("HOME", home.as_os_str());
524 env::set_var("USERPROFILE", home.as_os_str());
525 }
526 assert_ne!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
527 assert_eq!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
528 }
529 }
530}