1use 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
22pub trait ProvideEnv: Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
24 fn get(&self, k: &str) -> Result<String, VarError>;
26}
27
28pub trait ProvideFs: Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
30 fn read_to_end(
32 &self,
33 path: &Path,
34 ) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<u8>>> + Send + '_>>;
35
36 fn write(
38 &self,
39 path: &Path,
40 contents: &[u8],
41 ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>>;
42}
43
44#[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 pub fn real() -> Self {
75 Fs(fs::Inner::Real)
76 }
77
78 pub fn from_custom(provider: impl ProvideFs + 'static) -> Self {
80 Self(fs::Inner::Custom(Arc::new(provider)))
81 }
82
83 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 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 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 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 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 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 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 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#[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 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 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 pub fn real() -> Self {
306 Self(env::Inner::Real)
307 }
308
309 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; assert_eq!(mode, 0o600, "file should be owner read/write only");
480 }
481}