use crate::{BoxedFuture, Error, MaybeSend, Result};
use bytes::Bytes;
use std::collections::HashMap;
use std::fmt::Debug;
use std::future::Future;
use std::ops::Deref;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone)]
pub struct Context {
fs: Arc<dyn FileReadDyn>,
http: Arc<dyn HttpSendDyn>,
env: Arc<dyn Env>,
cmd: Arc<dyn CommandExecuteDyn>,
}
impl Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context")
.field("fs", &self.fs)
.field("http", &self.http)
.field("env", &self.env)
.field("cmd", &self.cmd)
.finish()
}
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
impl Context {
pub fn new() -> Self {
Self {
fs: Arc::new(NoopFileRead),
http: Arc::new(NoopHttpSend),
env: Arc::new(NoopEnv),
cmd: Arc::new(NoopCommandExecute),
}
}
pub fn with_file_read(mut self, fs: impl FileRead) -> Self {
self.fs = Arc::new(fs);
self
}
pub fn with_http_send(mut self, http: impl HttpSend) -> Self {
self.http = Arc::new(http);
self
}
pub fn with_env(mut self, env: impl Env) -> Self {
self.env = Arc::new(env);
self
}
pub fn with_command_execute(mut self, cmd: impl CommandExecute) -> Self {
self.cmd = Arc::new(cmd);
self
}
#[inline]
pub async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
self.fs.file_read_dyn(path).await
}
pub async fn file_read_as_string(&self, path: &str) -> Result<String> {
let bytes = self.file_read(path).await?;
Ok(String::from_utf8_lossy(&bytes).to_string())
}
#[inline]
pub async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
self.http.http_send_dyn(req).await
}
pub async fn http_send_as_string(
&self,
req: http::Request<Bytes>,
) -> Result<http::Response<String>> {
let (parts, body) = self.http.http_send_dyn(req).await?.into_parts();
let body = String::from_utf8_lossy(&body).to_string();
Ok(http::Response::from_parts(parts, body))
}
#[inline]
pub fn home_dir(&self) -> Option<PathBuf> {
self.env.home_dir()
}
pub fn expand_home_dir(&self, path: &str) -> Option<String> {
if !path.starts_with("~/") && !path.starts_with("~\\") {
Some(path.to_string())
} else {
self.home_dir()
.map(|home| path.replace('~', &home.to_string_lossy()))
}
}
#[inline]
pub fn env_var(&self, key: &str) -> Option<String> {
self.env.var(key)
}
#[inline]
pub fn env_vars(&self) -> HashMap<String, String> {
self.env.vars()
}
pub async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
self.cmd.command_execute_dyn(program, args).await
}
}
pub trait FileRead: Debug + Send + Sync + 'static {
fn file_read(&self, path: &str) -> impl Future<Output = Result<Vec<u8>>> + MaybeSend;
}
pub trait FileReadDyn: Debug + Send + Sync + 'static {
fn file_read_dyn<'a>(&'a self, path: &'a str) -> BoxedFuture<'a, Result<Vec<u8>>>;
}
impl<T: FileRead + ?Sized> FileReadDyn for T {
fn file_read_dyn<'a>(&'a self, path: &'a str) -> BoxedFuture<'a, Result<Vec<u8>>> {
Box::pin(self.file_read(path))
}
}
impl<T: FileReadDyn + ?Sized> FileRead for Arc<T> {
async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
self.deref().file_read_dyn(path).await
}
}
pub trait HttpSend: Debug + Send + Sync + 'static {
fn http_send(
&self,
req: http::Request<Bytes>,
) -> impl Future<Output = Result<http::Response<Bytes>>> + MaybeSend;
}
pub trait HttpSendDyn: Debug + Send + Sync + 'static {
fn http_send_dyn(
&self,
req: http::Request<Bytes>,
) -> BoxedFuture<'_, Result<http::Response<Bytes>>>;
}
impl<T: HttpSend + ?Sized> HttpSendDyn for T {
fn http_send_dyn(
&self,
req: http::Request<Bytes>,
) -> BoxedFuture<'_, Result<http::Response<Bytes>>> {
Box::pin(self.http_send(req))
}
}
impl<T: HttpSendDyn + ?Sized> HttpSend for Arc<T> {
async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
self.deref().http_send_dyn(req).await
}
}
pub trait Env: Debug + Send + Sync + 'static {
fn var(&self, key: &str) -> Option<String>;
fn vars(&self) -> HashMap<String, String>;
fn home_dir(&self) -> Option<PathBuf>;
}
#[derive(Debug, Copy, Clone)]
pub struct OsEnv;
impl Env for OsEnv {
fn var(&self, key: &str) -> Option<String> {
std::env::var_os(key)?.into_string().ok()
}
fn vars(&self) -> HashMap<String, String> {
std::env::vars().collect()
}
#[cfg(any(unix, target_os = "redox"))]
fn home_dir(&self) -> Option<PathBuf> {
#[allow(deprecated)]
std::env::home_dir()
}
#[cfg(windows)]
fn home_dir(&self) -> Option<PathBuf> {
windows::home_dir_inner()
}
#[cfg(target_arch = "wasm32")]
fn home_dir(&self) -> Option<PathBuf> {
None
}
}
#[derive(Debug, Clone, Default)]
pub struct StaticEnv {
pub home_dir: Option<PathBuf>,
pub envs: HashMap<String, String>,
}
impl Env for StaticEnv {
fn var(&self, key: &str) -> Option<String> {
self.envs.get(key).cloned()
}
fn vars(&self) -> HashMap<String, String> {
self.envs.clone()
}
fn home_dir(&self) -> Option<PathBuf> {
self.home_dir.clone()
}
}
#[derive(Debug, Clone)]
pub struct CommandOutput {
pub status: i32,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl CommandOutput {
pub fn success(&self) -> bool {
self.status == 0
}
}
pub trait CommandExecute: Debug + Send + Sync + 'static {
fn command_execute<'a>(
&'a self,
program: &'a str,
args: &'a [&'a str],
) -> impl Future<Output = Result<CommandOutput>> + MaybeSend + 'a;
}
pub trait CommandExecuteDyn: Debug + Send + Sync + 'static {
fn command_execute_dyn<'a>(
&'a self,
program: &'a str,
args: &'a [&'a str],
) -> BoxedFuture<'a, Result<CommandOutput>>;
}
impl<T: CommandExecute + ?Sized> CommandExecuteDyn for T {
fn command_execute_dyn<'a>(
&'a self,
program: &'a str,
args: &'a [&'a str],
) -> BoxedFuture<'a, Result<CommandOutput>> {
Box::pin(self.command_execute(program, args))
}
}
impl<T: CommandExecuteDyn + ?Sized> CommandExecute for Arc<T> {
async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
self.deref().command_execute_dyn(program, args).await
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopFileRead;
impl FileRead for NoopFileRead {
async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
Err(Error::unexpected(
"file reading not supported: no file reader configured",
))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopHttpSend;
impl HttpSend for NoopHttpSend {
async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
Err(Error::unexpected(
"HTTP sending not supported: no HTTP client configured",
))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopEnv;
impl Env for NoopEnv {
fn var(&self, _key: &str) -> Option<String> {
None
}
fn vars(&self) -> HashMap<String, String> {
HashMap::new()
}
fn home_dir(&self) -> Option<PathBuf> {
None
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopCommandExecute;
impl CommandExecute for NoopCommandExecute {
async fn command_execute(&self, _program: &str, _args: &[&str]) -> Result<CommandOutput> {
Err(Error::unexpected(
"command execution not supported: no command executor configured",
))
}
}
#[cfg(target_os = "windows")]
mod windows {
use std::env;
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::ptr;
use std::slice;
use windows_sys::Win32::Foundation::S_OK;
use windows_sys::Win32::System::Com::CoTaskMemFree;
use windows_sys::Win32::UI::Shell::{
FOLDERID_Profile, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
};
pub fn home_dir_inner() -> Option<PathBuf> {
env::var_os("USERPROFILE")
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(home_dir_crt)
}
#[cfg(not(target_vendor = "uwp"))]
fn home_dir_crt() -> Option<PathBuf> {
unsafe {
let mut path = ptr::null_mut();
match SHGetKnownFolderPath(
&FOLDERID_Profile,
KF_FLAG_DONT_VERIFY as u32,
std::ptr::null_mut(),
&mut path,
) {
S_OK => {
let path_slice = slice::from_raw_parts(path, wcslen(path));
let s = OsString::from_wide(&path_slice);
CoTaskMemFree(path.cast());
Some(PathBuf::from(s))
}
_ => {
CoTaskMemFree(path.cast());
None
}
}
}
}
#[cfg(target_vendor = "uwp")]
fn home_dir_crt() -> Option<PathBuf> {
None
}
unsafe extern "C" {
unsafe fn wcslen(buf: *const u16) -> usize;
}
#[cfg(not(target_vendor = "uwp"))]
#[cfg(test)]
mod tests {
use super::home_dir_inner;
use std::env;
use std::ops::Deref;
use std::path::{Path, PathBuf};
#[test]
fn test_with_without() {
let olduserprofile = env::var_os("USERPROFILE").unwrap();
unsafe {
env::remove_var("HOME");
env::remove_var("USERPROFILE");
}
assert_eq!(home_dir_inner(), Some(PathBuf::from(olduserprofile)));
let home = Path::new(r"C:\Users\foo tar baz");
unsafe {
env::set_var("HOME", home.as_os_str());
env::set_var("USERPROFILE", home.as_os_str());
}
assert_ne!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
assert_eq!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
}
}
}