aws_types/
os_shim_internal.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Abstractions for testing code that interacts with the operating system:
7//! - Reading environment variables
8//! - Reading from the file system
9
10use std::collections::HashMap;
11use std::env::VarError;
12use std::ffi::OsString;
13use std::fmt::Debug;
14use std::future::Future;
15use std::panic::{RefUnwindSafe, UnwindSafe};
16use std::path::{Path, PathBuf};
17use std::pin::Pin;
18use std::sync::{Arc, Mutex};
19
20use crate::os_shim_internal::fs::Fake;
21
22/// Trait for custom environment variable providers.
23pub trait ProvideEnv: Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
24    /// Get the value of environment variable `k`.
25    fn get(&self, k: &str) -> Result<String, VarError>;
26}
27
28/// Trait for custom filesystem providers.
29pub trait ProvideFs: Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
30    /// Read the entire contents of the file at `path`.
31    fn read_to_end(
32        &self,
33        path: &Path,
34    ) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<u8>>> + Send + '_>>;
35
36    /// Write `contents` to the file at `path`.
37    fn write(
38        &self,
39        path: &Path,
40        contents: &[u8],
41    ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>>;
42}
43
44/// File system abstraction
45///
46/// Simple abstraction enabling in-memory mocking of the file system
47///
48/// # Examples
49/// Construct a file system which delegates to `std::fs`:
50/// ```rust
51/// let fs = aws_types::os_shim_internal::Fs::real();
52/// ```
53///
54/// Construct an in-memory file system for testing:
55/// ```rust
56/// use std::collections::HashMap;
57/// let fs = aws_types::os_shim_internal::Fs::from_map({
58///     let mut map = HashMap::new();
59///     map.insert("/home/.aws/config".to_string(), "[default]\nregion = us-east-1");
60///     map
61/// });
62/// ```
63#[derive(Clone, Debug)]
64pub struct Fs(fs::Inner);
65
66impl Default for Fs {
67    fn default() -> Self {
68        Fs::real()
69    }
70}
71
72impl Fs {
73    /// Create `Fs` representing a real file system.
74    pub fn real() -> Self {
75        Fs(fs::Inner::Real)
76    }
77
78    /// Create an `Fs` backed by a custom `ProvideFs` implementation.
79    pub fn from_custom(provider: impl ProvideFs + 'static) -> Self {
80        Self(fs::Inner::Custom(Arc::new(provider)))
81    }
82
83    /// Create `Fs` from a map of `OsString` to `Vec<u8>`.
84    pub fn from_raw_map(fs: HashMap<OsString, Vec<u8>>) -> Self {
85        Fs(fs::Inner::Fake(Arc::new(Fake::MapFs(Mutex::new(fs)))))
86    }
87
88    /// Create `Fs` from a map of `String` to `Vec<u8>`.
89    pub fn from_map(data: HashMap<String, impl Into<Vec<u8>>>) -> Self {
90        let fs = data
91            .into_iter()
92            .map(|(k, v)| (k.into(), v.into()))
93            .collect();
94        Self::from_raw_map(fs)
95    }
96
97    /// Create a test filesystem rooted in real files
98    ///
99    /// Creates a test filesystem from the contents of `test_directory` rooted into `namespaced_to`.
100    ///
101    /// Example:
102    /// Given:
103    /// ```bash
104    /// $ ls
105    /// ./my-test-dir/aws-config
106    /// ./my-test-dir/aws-config/config
107    /// $ cat ./my-test-dir/aws-config/config
108    /// test-config
109    /// ```
110    /// ```rust,no_run
111    /// # async fn docs() {
112    /// use aws_types::os_shim_internal::{Env, Fs};
113    /// let env = Env::from_slice(&[("HOME", "/Users/me")]);
114    /// let fs = Fs::from_test_dir("my-test-dir/aws-config", "/Users/me/.aws/config");
115    /// assert_eq!(fs.read_to_end("/Users/me/.aws/config").await.unwrap(), b"test-config");
116    /// # }
117    pub fn from_test_dir(
118        test_directory: impl Into<PathBuf>,
119        namespaced_to: impl Into<PathBuf>,
120    ) -> Self {
121        Self(fs::Inner::Fake(Arc::new(Fake::NamespacedFs {
122            real_path: test_directory.into(),
123            namespaced_to: namespaced_to.into(),
124        })))
125    }
126
127    /// Create a fake process environment from a slice of tuples.
128    ///
129    /// # Examples
130    /// ```rust
131    /// # async fn example() {
132    /// use aws_types::os_shim_internal::Fs;
133    /// let mock_fs = Fs::from_slice(&[
134    ///     ("config", "[default]\nretry_mode = \"standard\""),
135    /// ]);
136    /// assert_eq!(mock_fs.read_to_end("config").await.unwrap(), b"[default]\nretry_mode = \"standard\"");
137    /// # }
138    /// ```
139    pub fn from_slice<'a>(files: &[(&'a str, &'a str)]) -> Self {
140        let fs: HashMap<String, Vec<u8>> = files
141            .iter()
142            .map(|(k, v)| {
143                let k = (*k).to_owned();
144                let v = v.as_bytes().to_vec();
145                (k, v)
146            })
147            .collect();
148
149        Self::from_map(fs)
150    }
151
152    /// Read the entire contents of a file
153    ///
154    /// _Note: This function is currently `async` primarily for forward compatibility. Currently,
155    /// this function does not use Tokio (or any other runtime) to perform IO, the IO is performed
156    /// directly within the function._
157    pub async fn read_to_end(&self, path: impl AsRef<Path>) -> std::io::Result<Vec<u8>> {
158        use fs::Inner;
159        let path = path.as_ref();
160        match &self.0 {
161            // TODO(https://github.com/awslabs/aws-sdk-rust/issues/867): Use async IO below
162            Inner::Real => std::fs::read(path),
163            Inner::Fake(fake) => match fake.as_ref() {
164                Fake::MapFs(fs) => fs
165                    .lock()
166                    .unwrap()
167                    .get(path.as_os_str())
168                    .cloned()
169                    .ok_or_else(|| std::io::ErrorKind::NotFound.into()),
170                Fake::NamespacedFs {
171                    real_path,
172                    namespaced_to,
173                } => {
174                    let actual_path = path
175                        .strip_prefix(namespaced_to)
176                        .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
177                    std::fs::read(real_path.join(actual_path))
178                }
179            },
180            Inner::Custom(provider) => provider.read_to_end(path).await,
181        }
182    }
183
184    /// Write a slice as the entire contents of a file.
185    ///
186    /// This is equivalent to `std::fs::write`.
187    pub async fn write(
188        &self,
189        path: impl AsRef<Path>,
190        contents: impl AsRef<[u8]>,
191    ) -> std::io::Result<()> {
192        use fs::Inner;
193        match &self.0 {
194            // TODO(https://github.com/awslabs/aws-sdk-rust/issues/867): Use async IO below
195            Inner::Real => {
196                std::fs::write(&path, contents)?;
197
198                #[cfg(unix)]
199                {
200                    use std::os::unix::fs::PermissionsExt;
201                    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
202                }
203            }
204            Inner::Fake(fake) => match fake.as_ref() {
205                Fake::MapFs(fs) => {
206                    fs.lock()
207                        .unwrap()
208                        .insert(path.as_ref().as_os_str().into(), contents.as_ref().to_vec());
209                }
210                Fake::NamespacedFs {
211                    real_path,
212                    namespaced_to,
213                } => {
214                    let actual_path = path
215                        .as_ref()
216                        .strip_prefix(namespaced_to)
217                        .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
218                    std::fs::write(real_path.join(actual_path), contents)?;
219                }
220            },
221            Inner::Custom(provider) => {
222                return provider.write(path.as_ref(), contents.as_ref()).await
223            }
224        }
225        Ok(())
226    }
227}
228
229mod fs {
230    use std::collections::HashMap;
231    use std::ffi::OsString;
232    use std::path::PathBuf;
233    use std::sync::{Arc, Mutex};
234
235    use super::ProvideFs;
236
237    #[derive(Clone, Debug)]
238    pub(super) enum Inner {
239        Real,
240        Fake(Arc<Fake>),
241        Custom(Arc<dyn ProvideFs>),
242    }
243
244    #[derive(Debug)]
245    pub(super) enum Fake {
246        MapFs(Mutex<HashMap<OsString, Vec<u8>>>),
247        NamespacedFs {
248            real_path: PathBuf,
249            namespaced_to: PathBuf,
250        },
251    }
252}
253
254/// Environment variable abstraction
255///
256/// Environment variables are global to a process, and, as such, are difficult to test with a multi-
257/// threaded test runner like Rust's. This enables loading environment variables either from the
258/// actual process environment ([`std::env::var`]) or from a hash map.
259///
260/// Process environments are cheap to clone:
261/// - Faked process environments are wrapped in an internal Arc
262/// - Real process environments are pointer-sized
263#[derive(Clone, Debug)]
264pub struct Env(env::Inner);
265
266impl Default for Env {
267    fn default() -> Self {
268        Self::real()
269    }
270}
271
272impl Env {
273    /// Retrieve a value for the given `k` and return `VarError` is that key is not present.
274    pub fn get(&self, k: &str) -> Result<String, VarError> {
275        use env::Inner;
276        match &self.0 {
277            Inner::Real => std::env::var(k),
278            Inner::Fake(map) => map.get(k).cloned().ok_or(VarError::NotPresent),
279            Inner::Custom(provider) => provider.get(k),
280        }
281    }
282
283    /// Create a fake process environment from a slice of tuples.
284    ///
285    /// # Examples
286    /// ```rust
287    /// use aws_types::os_shim_internal::Env;
288    /// let mock_env = Env::from_slice(&[
289    ///     ("HOME", "/home/myname"),
290    ///     ("AWS_REGION", "us-west-2")
291    /// ]);
292    /// assert_eq!(mock_env.get("HOME").unwrap(), "/home/myname");
293    /// ```
294    pub fn from_slice<'a>(vars: &[(&'a str, &'a str)]) -> Self {
295        let map: HashMap<_, _> = vars
296            .iter()
297            .map(|(k, v)| (k.to_string(), v.to_string()))
298            .collect();
299        Self::from(map)
300    }
301
302    /// Create a process environment that uses the real process environment
303    ///
304    /// Calls will be delegated to [`std::env::var`].
305    pub fn real() -> Self {
306        Self(env::Inner::Real)
307    }
308
309    /// Create an `Env` backed by a custom `ProvideEnv` implementation.
310    pub fn from_custom(provider: impl ProvideEnv + 'static) -> Self {
311        Self(env::Inner::Custom(Arc::new(provider)))
312    }
313}
314
315impl From<HashMap<String, String>> for Env {
316    fn from(hash_map: HashMap<String, String>) -> Self {
317        Self(env::Inner::Fake(Arc::new(hash_map)))
318    }
319}
320
321mod env {
322    use std::collections::HashMap;
323    use std::sync::Arc;
324
325    use super::ProvideEnv;
326
327    #[derive(Clone, Debug)]
328    pub(super) enum Inner {
329        Real,
330        Fake(Arc<HashMap<String, String>>),
331        Custom(Arc<dyn ProvideEnv>),
332    }
333}
334
335#[cfg(test)]
336mod test {
337    use std::collections::HashMap;
338    use std::env::VarError;
339    use std::future::Future;
340    use std::path::{Path, PathBuf};
341    use std::pin::Pin;
342    use std::sync::Mutex;
343
344    use crate::os_shim_internal::{Env, Fs, ProvideEnv, ProvideFs};
345
346    #[test]
347    fn env_works() {
348        let env = Env::from_slice(&[("FOO", "BAR")]);
349        assert_eq!(env.get("FOO").unwrap(), "BAR");
350        assert_eq!(
351            env.get("OTHER").expect_err("no present"),
352            VarError::NotPresent
353        )
354    }
355
356    #[tokio::test]
357    async fn fs_from_test_dir_works() {
358        let fs = Fs::from_test_dir(".", "/users/test-data");
359        let _ = fs
360            .read_to_end("/users/test-data/Cargo.toml")
361            .await
362            .expect("file exists");
363
364        let _ = fs
365            .read_to_end("doesntexist")
366            .await
367            .expect_err("file doesnt exists");
368    }
369
370    #[tokio::test]
371    async fn fs_round_trip_file_with_real() {
372        let temp = tempfile::tempdir().unwrap();
373        let path = temp.path().join("test-file");
374
375        let fs = Fs::real();
376        fs.read_to_end(&path)
377            .await
378            .expect_err("file doesn't exist yet");
379
380        fs.write(&path, b"test").await.expect("success");
381
382        let result = fs.read_to_end(&path).await.expect("success");
383        assert_eq!(b"test", &result[..]);
384    }
385
386    #[test]
387    fn custom_env_works() {
388        #[derive(Debug)]
389        struct CustomEnvProvider {
390            vars: HashMap<String, String>,
391        }
392
393        impl ProvideEnv for CustomEnvProvider {
394            fn get(&self, k: &str) -> Result<String, VarError> {
395                self.vars.get(k).cloned().ok_or(VarError::NotPresent)
396            }
397        }
398
399        let mut vars = HashMap::new();
400        vars.insert("FOO".to_string(), "BAR".to_string());
401        let env = Env::from_custom(CustomEnvProvider { vars });
402        assert_eq!(env.get("FOO").unwrap(), "BAR");
403        assert_eq!(
404            env.get("OTHER").expect_err("not present"),
405            VarError::NotPresent
406        );
407    }
408
409    #[tokio::test]
410    async fn custom_fs_round_trip() {
411        #[derive(Debug)]
412        struct InMemoryFs {
413            files: Mutex<HashMap<PathBuf, Vec<u8>>>,
414        }
415
416        impl ProvideFs for InMemoryFs {
417            fn read_to_end(
418                &self,
419                path: &Path,
420            ) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<u8>>> + Send + '_>> {
421                let path = path.to_path_buf();
422                Box::pin(async move {
423                    self.files
424                        .lock()
425                        .unwrap()
426                        .get(&path)
427                        .cloned()
428                        .ok_or_else(|| std::io::ErrorKind::NotFound.into())
429                })
430            }
431
432            fn write(
433                &self,
434                path: &Path,
435                contents: &[u8],
436            ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
437                let path = path.to_path_buf();
438                let contents = contents.to_vec();
439                Box::pin(async move {
440                    self.files.lock().unwrap().insert(path, contents);
441                    Ok(())
442                })
443            }
444        }
445
446        let provider = InMemoryFs {
447            files: Mutex::new(HashMap::new()),
448        };
449        let fs = Fs::from_custom(provider);
450
451        fs.read_to_end("/missing")
452            .await
453            .expect_err("file doesn't exist yet");
454
455        fs.write("/test-file", b"hello")
456            .await
457            .expect("write succeeds");
458
459        let result = fs.read_to_end("/test-file").await.expect("read succeeds");
460        assert_eq!(result, b"hello");
461    }
462
463    #[cfg(unix)]
464    #[tokio::test]
465    async fn real_fs_write_sets_owner_only_permissions_on_unix() {
466        use std::os::unix::fs::PermissionsExt;
467
468        let dir = tempfile::tempdir().expect("create temp dir");
469        let path = dir.path().join("secret.txt");
470        let fs = Fs::real();
471
472        fs.write(&path, b"sensitive").await.expect("write succeeds");
473
474        let mode = std::fs::metadata(&path)
475            .expect("metadata")
476            .permissions()
477            .mode()
478            & 0o777; // mask off file type bits, keep only permission bits
479        assert_eq!(mode, 0o600, "file should be owner read/write only");
480    }
481}