use std::collections::HashMap;
use std::env::VarError;
use std::ffi::OsString;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::os_shim_internal::fs::Fake;
#[derive(Clone, Debug)]
pub struct Fs(fs::Inner);
impl Default for Fs {
fn default() -> Self {
Fs::real()
}
}
impl Fs {
pub fn real() -> Self {
Fs(fs::Inner::Real)
}
pub fn from_raw_map(fs: HashMap<OsString, Vec<u8>>) -> Self {
Fs(fs::Inner::Fake(Arc::new(Fake::MapFs(Mutex::new(fs)))))
}
pub fn from_map(data: HashMap<String, impl Into<Vec<u8>>>) -> Self {
let fs = data
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
Self::from_raw_map(fs)
}
pub fn from_test_dir(
test_directory: impl Into<PathBuf>,
namespaced_to: impl Into<PathBuf>,
) -> Self {
Self(fs::Inner::Fake(Arc::new(Fake::NamespacedFs {
real_path: test_directory.into(),
namespaced_to: namespaced_to.into(),
})))
}
pub fn from_slice<'a>(files: &[(&'a str, &'a str)]) -> Self {
let fs: HashMap<String, Vec<u8>> = files
.iter()
.map(|(k, v)| {
let k = (*k).to_owned();
let v = v.as_bytes().to_vec();
(k, v)
})
.collect();
Self::from_map(fs)
}
pub async fn read_to_end(&self, path: impl AsRef<Path>) -> std::io::Result<Vec<u8>> {
use fs::Inner;
let path = path.as_ref();
match &self.0 {
Inner::Real => std::fs::read(path),
Inner::Fake(fake) => match fake.as_ref() {
Fake::MapFs(fs) => fs
.lock()
.unwrap()
.get(path.as_os_str())
.cloned()
.ok_or_else(|| std::io::ErrorKind::NotFound.into()),
Fake::NamespacedFs {
real_path,
namespaced_to,
} => {
let actual_path = path
.strip_prefix(namespaced_to)
.map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
std::fs::read(real_path.join(actual_path))
}
},
}
}
pub async fn write(
&self,
path: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
) -> std::io::Result<()> {
use fs::Inner;
match &self.0 {
Inner::Real => {
std::fs::write(&path, contents)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
}
}
Inner::Fake(fake) => match fake.as_ref() {
Fake::MapFs(fs) => {
fs.lock()
.unwrap()
.insert(path.as_ref().as_os_str().into(), contents.as_ref().to_vec());
}
Fake::NamespacedFs {
real_path,
namespaced_to,
} => {
let actual_path = path
.as_ref()
.strip_prefix(namespaced_to)
.map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
std::fs::write(real_path.join(actual_path), contents)?;
}
},
}
Ok(())
}
}
mod fs {
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]
pub(super) enum Inner {
Real,
Fake(Arc<Fake>),
}
#[derive(Debug)]
pub(super) enum Fake {
MapFs(Mutex<HashMap<OsString, Vec<u8>>>),
NamespacedFs {
real_path: PathBuf,
namespaced_to: PathBuf,
},
}
}
#[derive(Clone, Debug)]
pub struct Env(env::Inner);
impl Default for Env {
fn default() -> Self {
Self::real()
}
}
impl Env {
pub fn get(&self, k: &str) -> Result<String, VarError> {
use env::Inner;
match &self.0 {
Inner::Real => std::env::var(k),
Inner::Fake(map) => map.get(k).cloned().ok_or(VarError::NotPresent),
}
}
pub fn from_slice<'a>(vars: &[(&'a str, &'a str)]) -> Self {
let map: HashMap<_, _> = vars
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
Self::from(map)
}
pub fn real() -> Self {
Self(env::Inner::Real)
}
}
impl From<HashMap<String, String>> for Env {
fn from(hash_map: HashMap<String, String>) -> Self {
Self(env::Inner::Fake(Arc::new(hash_map)))
}
}
mod env {
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub(super) enum Inner {
Real,
Fake(Arc<HashMap<String, String>>),
}
}
#[cfg(test)]
mod test {
use std::env::VarError;
use crate::os_shim_internal::{Env, Fs};
#[test]
fn env_works() {
let env = Env::from_slice(&[("FOO", "BAR")]);
assert_eq!(env.get("FOO").unwrap(), "BAR");
assert_eq!(
env.get("OTHER").expect_err("no present"),
VarError::NotPresent
)
}
#[tokio::test]
async fn fs_from_test_dir_works() {
let fs = Fs::from_test_dir(".", "/users/test-data");
let _ = fs
.read_to_end("/users/test-data/Cargo.toml")
.await
.expect("file exists");
let _ = fs
.read_to_end("doesntexist")
.await
.expect_err("file doesnt exists");
}
#[tokio::test]
async fn fs_round_trip_file_with_real() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("test-file");
let fs = Fs::real();
fs.read_to_end(&path)
.await
.expect_err("file doesn't exist yet");
fs.write(&path, b"test").await.expect("success");
let result = fs.read_to_end(&path).await.expect("success");
assert_eq!(b"test", &result[..]);
}
#[cfg(unix)]
#[tokio::test]
async fn real_fs_write_sets_owner_only_permissions_on_unix() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("secret.txt");
let fs = Fs::real();
fs.write(&path, b"sensitive").await.expect("write succeeds");
let mode = std::fs::metadata(&path)
.expect("metadata")
.permissions()
.mode()
& 0o777; assert_eq!(mode, 0o600, "file should be owner read/write only");
}
}