reqsign_core/
context.rs

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/// Context provides the context for the request signing.
9///
10/// ## Important
11///
12/// reqsign provides NO default implementations. Users MAY configure components they need.
13/// Any unconfigured component will use a no-op implementation that returns errors or empty values when called.
14///
15/// ## Example
16///
17/// ```
18/// use reqsign_core::{Context, OsEnv};
19///
20/// // Create a context with explicit implementations
21/// let ctx = Context::new()
22///     .with_env(OsEnv);  // Optionally configure environment implementation
23/// ```
24#[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    /// Create a new Context with no-op implementations.
51    ///
52    /// All components use no-op implementations by default.
53    /// Use the `with_*` methods to configure the components you need.
54    ///
55    /// ```
56    /// use reqsign_core::Context;
57    ///
58    /// let ctx = Context::new();
59    /// // All components use no-op implementations by default
60    /// // You can configure specific components as needed:
61    /// // ctx.with_file_read(my_file_reader)
62    /// //    .with_http_send(my_http_client)
63    /// //    .with_env(my_env_provider);
64    /// ```
65    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    /// Replace the file reader implementation.
75    pub fn with_file_read(mut self, fs: impl FileRead) -> Self {
76        self.fs = Arc::new(fs);
77        self
78    }
79
80    /// Replace the HTTP client implementation.
81    pub fn with_http_send(mut self, http: impl HttpSend) -> Self {
82        self.http = Arc::new(http);
83        self
84    }
85
86    /// Replace the environment implementation.
87    pub fn with_env(mut self, env: impl Env) -> Self {
88        self.env = Arc::new(env);
89        self
90    }
91
92    /// Replace the command executor implementation.
93    pub fn with_command_execute(mut self, cmd: impl CommandExecute) -> Self {
94        self.cmd = Arc::new(cmd);
95        self
96    }
97
98    /// Read the file content entirely in `Vec<u8>`.
99    #[inline]
100    pub async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
101        self.fs.file_read(path).await
102    }
103
104    /// Read the file content entirely in `String`.
105    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    /// Send http request and return the response.
111    #[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    /// Send http request and return the response as string.
117    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    /// Get the home directory of the current user.
127    #[inline]
128    pub fn home_dir(&self) -> Option<PathBuf> {
129        self.env.home_dir()
130    }
131
132    /// Expand `~` in input path.
133    ///
134    /// - If path not starts with `~/` or `~\\`, returns `Some(path)` directly.
135    /// - Otherwise, replace `~` with home dir instead.
136    /// - If home_dir is not found, returns `None`.
137    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    /// Get the environment variable.
147    ///
148    /// - Returns `Some(v)` if the environment variable is found and is valid utf-8.
149    /// - Returns `None` if the environment variable is not found or value is invalid.
150    #[inline]
151    pub fn env_var(&self, key: &str) -> Option<String> {
152        self.env.var(key)
153    }
154
155    /// Returns an hashmap of (variable, value) pairs of strings, for all the
156    /// environment variables of the current process.
157    #[inline]
158    pub fn env_vars(&self) -> HashMap<String, String> {
159        self.env.vars()
160    }
161
162    /// Execute an external command with the given program and arguments.
163    ///
164    /// Returns the command output including exit status, stdout, and stderr.
165    pub async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
166        self.cmd.command_execute(program, args).await
167    }
168}
169
170/// FileRead is used to read the file content entirely in `Vec<u8>`.
171///
172/// This could be used by `Load` to load the credential from the file.
173#[async_trait::async_trait]
174pub trait FileRead: Debug + Send + Sync + 'static {
175    /// Read the file content entirely in `Vec<u8>`.
176    async fn file_read(&self, path: &str) -> Result<Vec<u8>>;
177}
178
179/// HttpSend is used to send http request during the signing process.
180///
181/// For example, fetch IMDS token from AWS or OAuth2 refresh token. This trait is designed
182/// especially for the signer, please don't use it as a general http client.
183#[async_trait::async_trait]
184pub trait HttpSend: Debug + Send + Sync + 'static {
185    /// Send http request and return the response.
186    async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>>;
187}
188
189/// Permits parameterizing the home functions via the _from variants
190pub trait Env: Debug + Send + Sync + 'static {
191    /// Get an environment variable.
192    ///
193    /// - Returns `Some(v)` if the environment variable is found and is valid utf-8.
194    /// - Returns `None` if the environment variable is not found or value is invalid.
195    fn var(&self, key: &str) -> Option<String>;
196
197    /// Returns an hashmap of (variable, value) pairs of strings, for all the
198    /// environment variables of the current process.
199    fn vars(&self) -> HashMap<String, String>;
200
201    /// Return the path to the users home dir, returns `None` if any error occurs.
202    fn home_dir(&self) -> Option<PathBuf>;
203}
204
205/// Implements Env for the OS context, both Unix style and Windows.
206#[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/// StaticEnv provides a static env environment.
236///
237/// This is useful for testing or for providing a fixed environment.
238#[derive(Debug, Clone, Default)]
239pub struct StaticEnv {
240    /// The home directory to use.
241    pub home_dir: Option<PathBuf>,
242    /// The environment variables to use.
243    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/// CommandOutput represents the output of a command execution.
261#[derive(Debug, Clone)]
262pub struct CommandOutput {
263    /// Exit status code (0 for success)
264    pub status: i32,
265    /// Standard output as bytes
266    pub stdout: Vec<u8>,
267    /// Standard error as bytes
268    pub stderr: Vec<u8>,
269}
270
271impl CommandOutput {
272    /// Check if the command exited successfully.
273    pub fn success(&self) -> bool {
274        self.status == 0
275    }
276}
277
278/// CommandExecute is used to execute external commands for credential retrieval.
279///
280/// This trait abstracts command execution to support different runtime environments:
281/// - Tokio-based async execution
282/// - Blocking execution for non-async contexts
283/// - WebAssembly environments (returning errors)
284/// - Mock implementations for testing
285#[async_trait::async_trait]
286pub trait CommandExecute: Debug + Send + Sync + 'static {
287    /// Execute a command with the given program and arguments.
288    async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput>;
289}
290
291/// NoopFileRead is a no-op implementation that always returns an error.
292///
293/// This is used when no file reader is configured.
294#[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/// NoopHttpSend is a no-op implementation that always returns an error.
307///
308/// This is used when no HTTP client is configured.
309#[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/// NoopEnv is a no-op implementation that always returns None/empty.
322///
323/// This is used when no environment is configured.
324#[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/// NoopCommandExecute is a no-op implementation that always returns an error.
342///
343/// This is used when no command executor is configured.
344#[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                    // Free any allocated memory even on failure. A null ptr is a no-op for `CoTaskMemFree`.
396                    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}