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 {
220 fn http_send(
222 &self,
223 req: http::Request<Bytes>,
224 ) -> impl Future<Output = Result<http::Response<Bytes>>> + MaybeSend;
225}
226
227pub trait HttpSendDyn: Debug + Send + Sync + 'static {
229 fn http_send_dyn(
231 &self,
232 req: http::Request<Bytes>,
233 ) -> BoxedFuture<'_, Result<http::Response<Bytes>>>;
234}
235
236impl<T: HttpSend + ?Sized> HttpSendDyn for T {
237 fn http_send_dyn(
238 &self,
239 req: http::Request<Bytes>,
240 ) -> BoxedFuture<'_, Result<http::Response<Bytes>>> {
241 Box::pin(self.http_send(req))
242 }
243}
244
245impl<T: HttpSendDyn + ?Sized> HttpSend for Arc<T> {
246 async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
247 self.deref().http_send_dyn(req).await
248 }
249}
250
251pub trait Env: Debug + Send + Sync + 'static {
253 fn var(&self, key: &str) -> Option<String>;
258
259 fn vars(&self) -> HashMap<String, String>;
262
263 fn home_dir(&self) -> Option<PathBuf>;
265}
266
267#[derive(Debug, Copy, Clone)]
269pub struct OsEnv;
270
271impl Env for OsEnv {
272 fn var(&self, key: &str) -> Option<String> {
273 std::env::var_os(key)?.into_string().ok()
274 }
275
276 fn vars(&self) -> HashMap<String, String> {
277 std::env::vars().collect()
278 }
279
280 #[cfg(any(unix, target_os = "redox"))]
281 fn home_dir(&self) -> Option<PathBuf> {
282 #[allow(deprecated)]
283 std::env::home_dir()
284 }
285
286 #[cfg(windows)]
287 fn home_dir(&self) -> Option<PathBuf> {
288 windows::home_dir_inner()
289 }
290
291 #[cfg(target_arch = "wasm32")]
292 fn home_dir(&self) -> Option<PathBuf> {
293 None
294 }
295}
296
297#[derive(Debug, Clone, Default)]
301pub struct StaticEnv {
302 pub home_dir: Option<PathBuf>,
304 pub envs: HashMap<String, String>,
306}
307
308impl Env for StaticEnv {
309 fn var(&self, key: &str) -> Option<String> {
310 self.envs.get(key).cloned()
311 }
312
313 fn vars(&self) -> HashMap<String, String> {
314 self.envs.clone()
315 }
316
317 fn home_dir(&self) -> Option<PathBuf> {
318 self.home_dir.clone()
319 }
320}
321
322#[derive(Debug, Clone)]
324pub struct CommandOutput {
325 pub status: i32,
327 pub stdout: Vec<u8>,
329 pub stderr: Vec<u8>,
331}
332
333impl CommandOutput {
334 pub fn success(&self) -> bool {
336 self.status == 0
337 }
338}
339
340pub trait CommandExecute: Debug + Send + Sync + 'static {
348 fn command_execute<'a>(
350 &'a self,
351 program: &'a str,
352 args: &'a [&'a str],
353 ) -> impl Future<Output = Result<CommandOutput>> + MaybeSend + 'a;
354}
355
356pub trait CommandExecuteDyn: Debug + Send + Sync + 'static {
358 fn command_execute_dyn<'a>(
360 &'a self,
361 program: &'a str,
362 args: &'a [&'a str],
363 ) -> BoxedFuture<'a, Result<CommandOutput>>;
364}
365
366impl<T: CommandExecute + ?Sized> CommandExecuteDyn for T {
367 fn command_execute_dyn<'a>(
368 &'a self,
369 program: &'a str,
370 args: &'a [&'a str],
371 ) -> BoxedFuture<'a, Result<CommandOutput>> {
372 Box::pin(self.command_execute(program, args))
373 }
374}
375
376impl<T: CommandExecuteDyn + ?Sized> CommandExecute for Arc<T> {
377 async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
378 self.deref().command_execute_dyn(program, args).await
379 }
380}
381
382#[derive(Debug, Clone, Copy, Default)]
386pub struct NoopFileRead;
387
388impl FileRead for NoopFileRead {
389 async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
390 Err(Error::unexpected(
391 "file reading not supported: no file reader configured",
392 ))
393 }
394}
395
396#[derive(Debug, Clone, Copy, Default)]
400pub struct NoopHttpSend;
401
402impl HttpSend for NoopHttpSend {
403 async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
404 Err(Error::unexpected(
405 "HTTP sending not supported: no HTTP client configured",
406 ))
407 }
408}
409
410#[derive(Debug, Clone, Copy, Default)]
414pub struct NoopEnv;
415
416impl Env for NoopEnv {
417 fn var(&self, _key: &str) -> Option<String> {
418 None
419 }
420
421 fn vars(&self) -> HashMap<String, String> {
422 HashMap::new()
423 }
424
425 fn home_dir(&self) -> Option<PathBuf> {
426 None
427 }
428}
429
430#[derive(Debug, Clone, Copy, Default)]
434pub struct NoopCommandExecute;
435
436impl CommandExecute for NoopCommandExecute {
437 async fn command_execute(&self, _program: &str, _args: &[&str]) -> Result<CommandOutput> {
438 Err(Error::unexpected(
439 "command execution not supported: no command executor configured",
440 ))
441 }
442}
443
444#[cfg(target_os = "windows")]
445mod windows {
446 use std::env;
447 use std::ffi::OsString;
448 use std::os::windows::ffi::OsStringExt;
449 use std::path::PathBuf;
450 use std::ptr;
451 use std::slice;
452
453 use windows_sys::Win32::Foundation::S_OK;
454 use windows_sys::Win32::System::Com::CoTaskMemFree;
455 use windows_sys::Win32::UI::Shell::{
456 FOLDERID_Profile, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
457 };
458
459 pub fn home_dir_inner() -> Option<PathBuf> {
460 env::var_os("USERPROFILE")
461 .filter(|s| !s.is_empty())
462 .map(PathBuf::from)
463 .or_else(home_dir_crt)
464 }
465
466 #[cfg(not(target_vendor = "uwp"))]
467 fn home_dir_crt() -> Option<PathBuf> {
468 unsafe {
469 let mut path = ptr::null_mut();
470 match SHGetKnownFolderPath(
471 &FOLDERID_Profile,
472 KF_FLAG_DONT_VERIFY as u32,
473 std::ptr::null_mut(),
474 &mut path,
475 ) {
476 S_OK => {
477 let path_slice = slice::from_raw_parts(path, wcslen(path));
478 let s = OsString::from_wide(&path_slice);
479 CoTaskMemFree(path.cast());
480 Some(PathBuf::from(s))
481 }
482 _ => {
483 CoTaskMemFree(path.cast());
485 None
486 }
487 }
488 }
489 }
490
491 #[cfg(target_vendor = "uwp")]
492 fn home_dir_crt() -> Option<PathBuf> {
493 None
494 }
495
496 unsafe extern "C" {
497 unsafe fn wcslen(buf: *const u16) -> usize;
498 }
499
500 #[cfg(not(target_vendor = "uwp"))]
501 #[cfg(test)]
502 mod tests {
503 use super::home_dir_inner;
504 use std::env;
505 use std::ops::Deref;
506 use std::path::{Path, PathBuf};
507
508 #[test]
509 fn test_with_without() {
510 let olduserprofile = env::var_os("USERPROFILE").unwrap();
511 unsafe {
512 env::remove_var("HOME");
513 env::remove_var("USERPROFILE");
514 }
515 assert_eq!(home_dir_inner(), Some(PathBuf::from(olduserprofile)));
516
517 let home = Path::new(r"C:\Users\foo tar baz");
518 unsafe {
519 env::set_var("HOME", home.as_os_str());
520 env::set_var("USERPROFILE", home.as_os_str());
521 }
522 assert_ne!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
523 assert_eq!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
524 }
525 }
526}