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::path::{Path, PathBuf};
15use std::sync::{Arc, Mutex};
16
17use crate::os_shim_internal::fs::Fake;
18
19/// File system abstraction
20///
21/// Simple abstraction enabling in-memory mocking of the file system
22///
23/// # Examples
24/// Construct a file system which delegates to `std::fs`:
25/// ```rust
26/// let fs = aws_types::os_shim_internal::Fs::real();
27/// ```
28///
29/// Construct an in-memory file system for testing:
30/// ```rust
31/// use std::collections::HashMap;
32/// let fs = aws_types::os_shim_internal::Fs::from_map({
33///     let mut map = HashMap::new();
34///     map.insert("/home/.aws/config".to_string(), "[default]\nregion = us-east-1");
35///     map
36/// });
37/// ```
38#[derive(Clone, Debug)]
39pub struct Fs(fs::Inner);
40
41impl Default for Fs {
42    fn default() -> Self {
43        Fs::real()
44    }
45}
46
47impl Fs {
48    /// Create `Fs` representing a real file system.
49    pub fn real() -> Self {
50        Fs(fs::Inner::Real)
51    }
52
53    /// Create `Fs` from a map of `OsString` to `Vec<u8>`.
54    pub fn from_raw_map(fs: HashMap<OsString, Vec<u8>>) -> Self {
55        Fs(fs::Inner::Fake(Arc::new(Fake::MapFs(Mutex::new(fs)))))
56    }
57
58    /// Create `Fs` from a map of `String` to `Vec<u8>`.
59    pub fn from_map(data: HashMap<String, impl Into<Vec<u8>>>) -> Self {
60        let fs = data
61            .into_iter()
62            .map(|(k, v)| (k.into(), v.into()))
63            .collect();
64        Self::from_raw_map(fs)
65    }
66
67    /// Create a test filesystem rooted in real files
68    ///
69    /// Creates a test filesystem from the contents of `test_directory` rooted into `namespaced_to`.
70    ///
71    /// Example:
72    /// Given:
73    /// ```bash
74    /// $ ls
75    /// ./my-test-dir/aws-config
76    /// ./my-test-dir/aws-config/config
77    /// $ cat ./my-test-dir/aws-config/config
78    /// test-config
79    /// ```
80    /// ```rust,no_run
81    /// # async fn docs() {
82    /// use aws_types::os_shim_internal::{Env, Fs};
83    /// let env = Env::from_slice(&[("HOME", "/Users/me")]);
84    /// let fs = Fs::from_test_dir("my-test-dir/aws-config", "/Users/me/.aws/config");
85    /// assert_eq!(fs.read_to_end("/Users/me/.aws/config").await.unwrap(), b"test-config");
86    /// # }
87    pub fn from_test_dir(
88        test_directory: impl Into<PathBuf>,
89        namespaced_to: impl Into<PathBuf>,
90    ) -> Self {
91        Self(fs::Inner::Fake(Arc::new(Fake::NamespacedFs {
92            real_path: test_directory.into(),
93            namespaced_to: namespaced_to.into(),
94        })))
95    }
96
97    /// Create a fake process environment from a slice of tuples.
98    ///
99    /// # Examples
100    /// ```rust
101    /// # async fn example() {
102    /// use aws_types::os_shim_internal::Fs;
103    /// let mock_fs = Fs::from_slice(&[
104    ///     ("config", "[default]\nretry_mode = \"standard\""),
105    /// ]);
106    /// assert_eq!(mock_fs.read_to_end("config").await.unwrap(), b"[default]\nretry_mode = \"standard\"");
107    /// # }
108    /// ```
109    pub fn from_slice<'a>(files: &[(&'a str, &'a str)]) -> Self {
110        let fs: HashMap<String, Vec<u8>> = files
111            .iter()
112            .map(|(k, v)| {
113                let k = (*k).to_owned();
114                let v = v.as_bytes().to_vec();
115                (k, v)
116            })
117            .collect();
118
119        Self::from_map(fs)
120    }
121
122    /// Read the entire contents of a file
123    ///
124    /// _Note: This function is currently `async` primarily for forward compatibility. Currently,
125    /// this function does not use Tokio (or any other runtime) to perform IO, the IO is performed
126    /// directly within the function._
127    pub async fn read_to_end(&self, path: impl AsRef<Path>) -> std::io::Result<Vec<u8>> {
128        use fs::Inner;
129        let path = path.as_ref();
130        match &self.0 {
131            // TODO(https://github.com/awslabs/aws-sdk-rust/issues/867): Use async IO below
132            Inner::Real => std::fs::read(path),
133            Inner::Fake(fake) => match fake.as_ref() {
134                Fake::MapFs(fs) => fs
135                    .lock()
136                    .unwrap()
137                    .get(path.as_os_str())
138                    .cloned()
139                    .ok_or_else(|| std::io::ErrorKind::NotFound.into()),
140                Fake::NamespacedFs {
141                    real_path,
142                    namespaced_to,
143                } => {
144                    let actual_path = path
145                        .strip_prefix(namespaced_to)
146                        .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
147                    std::fs::read(real_path.join(actual_path))
148                }
149            },
150        }
151    }
152
153    /// Write a slice as the entire contents of a file.
154    ///
155    /// This is equivalent to `std::fs::write`.
156    pub async fn write(
157        &self,
158        path: impl AsRef<Path>,
159        contents: impl AsRef<[u8]>,
160    ) -> std::io::Result<()> {
161        use fs::Inner;
162        match &self.0 {
163            // TODO(https://github.com/awslabs/aws-sdk-rust/issues/867): Use async IO below
164            Inner::Real => {
165                std::fs::write(&path, contents)?;
166
167                #[cfg(unix)]
168                {
169                    use std::os::unix::fs::PermissionsExt;
170                    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
171                }
172            }
173            Inner::Fake(fake) => match fake.as_ref() {
174                Fake::MapFs(fs) => {
175                    fs.lock()
176                        .unwrap()
177                        .insert(path.as_ref().as_os_str().into(), contents.as_ref().to_vec());
178                }
179                Fake::NamespacedFs {
180                    real_path,
181                    namespaced_to,
182                } => {
183                    let actual_path = path
184                        .as_ref()
185                        .strip_prefix(namespaced_to)
186                        .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
187                    std::fs::write(real_path.join(actual_path), contents)?;
188                }
189            },
190        }
191        Ok(())
192    }
193}
194
195mod fs {
196    use std::collections::HashMap;
197    use std::ffi::OsString;
198    use std::path::PathBuf;
199    use std::sync::{Arc, Mutex};
200
201    #[derive(Clone, Debug)]
202    pub(super) enum Inner {
203        Real,
204        Fake(Arc<Fake>),
205    }
206
207    #[derive(Debug)]
208    pub(super) enum Fake {
209        MapFs(Mutex<HashMap<OsString, Vec<u8>>>),
210        NamespacedFs {
211            real_path: PathBuf,
212            namespaced_to: PathBuf,
213        },
214    }
215}
216
217/// Environment variable abstraction
218///
219/// Environment variables are global to a process, and, as such, are difficult to test with a multi-
220/// threaded test runner like Rust's. This enables loading environment variables either from the
221/// actual process environment ([`std::env::var`]) or from a hash map.
222///
223/// Process environments are cheap to clone:
224/// - Faked process environments are wrapped in an internal Arc
225/// - Real process environments are pointer-sized
226#[derive(Clone, Debug)]
227pub struct Env(env::Inner);
228
229impl Default for Env {
230    fn default() -> Self {
231        Self::real()
232    }
233}
234
235impl Env {
236    /// Retrieve a value for the given `k` and return `VarError` is that key is not present.
237    pub fn get(&self, k: &str) -> Result<String, VarError> {
238        use env::Inner;
239        match &self.0 {
240            Inner::Real => std::env::var(k),
241            Inner::Fake(map) => map.get(k).cloned().ok_or(VarError::NotPresent),
242        }
243    }
244
245    /// Create a fake process environment from a slice of tuples.
246    ///
247    /// # Examples
248    /// ```rust
249    /// use aws_types::os_shim_internal::Env;
250    /// let mock_env = Env::from_slice(&[
251    ///     ("HOME", "/home/myname"),
252    ///     ("AWS_REGION", "us-west-2")
253    /// ]);
254    /// assert_eq!(mock_env.get("HOME").unwrap(), "/home/myname");
255    /// ```
256    pub fn from_slice<'a>(vars: &[(&'a str, &'a str)]) -> Self {
257        let map: HashMap<_, _> = vars
258            .iter()
259            .map(|(k, v)| (k.to_string(), v.to_string()))
260            .collect();
261        Self::from(map)
262    }
263
264    /// Create a process environment that uses the real process environment
265    ///
266    /// Calls will be delegated to [`std::env::var`].
267    pub fn real() -> Self {
268        Self(env::Inner::Real)
269    }
270}
271
272impl From<HashMap<String, String>> for Env {
273    fn from(hash_map: HashMap<String, String>) -> Self {
274        Self(env::Inner::Fake(Arc::new(hash_map)))
275    }
276}
277
278mod env {
279    use std::collections::HashMap;
280    use std::sync::Arc;
281
282    #[derive(Clone, Debug)]
283    pub(super) enum Inner {
284        Real,
285        Fake(Arc<HashMap<String, String>>),
286    }
287}
288
289#[cfg(test)]
290mod test {
291    use std::env::VarError;
292
293    use crate::os_shim_internal::{Env, Fs};
294
295    #[test]
296    fn env_works() {
297        let env = Env::from_slice(&[("FOO", "BAR")]);
298        assert_eq!(env.get("FOO").unwrap(), "BAR");
299        assert_eq!(
300            env.get("OTHER").expect_err("no present"),
301            VarError::NotPresent
302        )
303    }
304
305    #[tokio::test]
306    async fn fs_from_test_dir_works() {
307        let fs = Fs::from_test_dir(".", "/users/test-data");
308        let _ = fs
309            .read_to_end("/users/test-data/Cargo.toml")
310            .await
311            .expect("file exists");
312
313        let _ = fs
314            .read_to_end("doesntexist")
315            .await
316            .expect_err("file doesnt exists");
317    }
318
319    #[tokio::test]
320    async fn fs_round_trip_file_with_real() {
321        let temp = tempfile::tempdir().unwrap();
322        let path = temp.path().join("test-file");
323
324        let fs = Fs::real();
325        fs.read_to_end(&path)
326            .await
327            .expect_err("file doesn't exist yet");
328
329        fs.write(&path, b"test").await.expect("success");
330
331        let result = fs.read_to_end(&path).await.expect("success");
332        assert_eq!(b"test", &result[..]);
333    }
334
335    #[cfg(unix)]
336    #[tokio::test]
337    async fn real_fs_write_sets_owner_only_permissions_on_unix() {
338        use std::os::unix::fs::PermissionsExt;
339
340        let dir = tempfile::tempdir().expect("create temp dir");
341        let path = dir.path().join("secret.txt");
342        let fs = Fs::real();
343
344        fs.write(&path, b"sensitive").await.expect("write succeeds");
345
346        let mode = std::fs::metadata(&path)
347            .expect("metadata")
348            .permissions()
349            .mode()
350            & 0o777; // mask off file type bits, keep only permission bits
351        assert_eq!(mode, 0o600, "file should be owner read/write only");
352    }
353}