Skip to main content

bitwarden_access_connector/
sys.rs

1//! Seams for the ambient system state the connector reads.
2
3use std::{ffi::OsString, path::Path, sync::Arc};
4
5/// Read-only access to the process environment.
6pub(crate) trait EnvSource: Send + Sync {
7    /// The value of a single variable, or `None` if it is unset.
8    fn var(&self, key: &str) -> Option<OsString>;
9
10    /// Every variable as a `(name, value)` pair. `SystemEnv` reads `std::env::vars`, which panics
11    /// on a non-Unicode entry.
12    fn vars(&self) -> Vec<(String, String)>;
13}
14
15/// Reads the real process environment.
16pub(crate) struct SystemEnv;
17
18impl EnvSource for SystemEnv {
19    fn var(&self, key: &str) -> Option<OsString> {
20        std::env::var_os(key)
21    }
22
23    fn vars(&self) -> Vec<(String, String)> {
24        std::env::vars().collect()
25    }
26}
27
28/// An in-memory environment for tests.
29#[cfg(test)]
30pub(crate) struct FakeEnv {
31    vars: std::collections::HashMap<String, String>,
32}
33
34#[cfg(test)]
35impl FakeEnv {
36    /// An environment containing nothing at all.
37    pub(crate) fn empty() -> Self {
38        Self {
39            vars: std::collections::HashMap::new(),
40        }
41    }
42
43    /// Adds one variable, chainable.
44    pub(crate) fn with(mut self, key: &str, value: &str) -> Self {
45        self.vars.insert(key.to_string(), value.to_string());
46        self
47    }
48}
49
50#[cfg(test)]
51impl<const N: usize> From<[(&str, &str); N]> for FakeEnv {
52    fn from(pairs: [(&str, &str); N]) -> Self {
53        pairs
54            .into_iter()
55            .fold(FakeEnv::empty(), |env, (k, v)| env.with(k, v))
56    }
57}
58
59#[cfg(test)]
60impl EnvSource for FakeEnv {
61    fn var(&self, key: &str) -> Option<OsString> {
62        self.vars.get(key).map(OsString::from)
63    }
64
65    fn vars(&self) -> Vec<(String, String)> {
66        self.vars
67            .iter()
68            .map(|(k, v)| (k.clone(), v.clone()))
69            .collect()
70    }
71}
72
73/// Read-only probing of the filesystem.
74pub(crate) trait FileSystem: Send + Sync {
75    /// Whether `path` exists and is a file.
76    fn is_file(&self, path: &Path) -> bool;
77
78    /// Resolves `path` to an absolute form with symlinks and `..` removed.
79    fn canonicalize(&self, path: &Path) -> std::io::Result<std::path::PathBuf>;
80}
81
82/// Probes the real filesystem.
83pub(crate) struct SystemFs;
84
85impl FileSystem for SystemFs {
86    fn is_file(&self, path: &Path) -> bool {
87        path.is_file()
88    }
89
90    fn canonicalize(&self, path: &Path) -> std::io::Result<std::path::PathBuf> {
91        path.canonicalize()
92    }
93}
94
95/// An in-memory filesystem for tests.
96#[cfg(test)]
97pub(crate) struct FakeFs {
98    files: std::collections::HashSet<std::path::PathBuf>,
99    canonical: std::collections::HashMap<std::path::PathBuf, std::path::PathBuf>,
100}
101
102#[cfg(test)]
103impl FakeFs {
104    /// A filesystem containing nothing at all.
105    pub(crate) fn empty() -> Self {
106        Self {
107            files: std::collections::HashSet::new(),
108            canonical: std::collections::HashMap::new(),
109        }
110    }
111
112    /// Adds one file, which canonicalises to itself.
113    pub(crate) fn with_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
114        let path = path.into();
115        self.files.insert(path.clone());
116        self.canonical.insert(path.clone(), path);
117        self
118    }
119
120    /// Adds a directory, which canonicalises to itself but is not a file.
121    pub(crate) fn with_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
122        let path = path.into();
123        self.canonical.insert(path.clone(), path);
124        self
125    }
126
127    /// Adds a symlink: `from` canonicalises to `to`.
128    pub(crate) fn with_link(
129        mut self,
130        from: impl Into<std::path::PathBuf>,
131        to: impl Into<std::path::PathBuf>,
132    ) -> Self {
133        let (from, to) = (from.into(), to.into());
134        self.files.insert(from.clone());
135        self.canonical.insert(from, to);
136        self
137    }
138}
139
140#[cfg(test)]
141impl FileSystem for FakeFs {
142    fn is_file(&self, path: &Path) -> bool {
143        self.files.contains(path)
144    }
145
146    fn canonicalize(&self, path: &Path) -> std::io::Result<std::path::PathBuf> {
147        self.canonical.get(path).cloned().ok_or_else(|| {
148            std::io::Error::new(std::io::ErrorKind::NotFound, "no such path in FakeFs")
149        })
150    }
151}
152
153/// The ambient system state the scripting layer depends on. Each half is its own trait, so a test
154/// can fake one and keep the other real.
155#[derive(Clone)]
156pub(crate) struct Platform {
157    pub(crate) env: Arc<dyn EnvSource>,
158    pub(crate) fs: Arc<dyn FileSystem>,
159}
160
161impl Platform {
162    /// The real environment and the real filesystem.
163    pub(crate) fn system() -> Self {
164        Self {
165            env: Arc::new(SystemEnv),
166            fs: Arc::new(SystemFs),
167        }
168    }
169}
170
171#[cfg(test)]
172impl Platform {
173    pub(crate) fn fake(env: FakeEnv, fs: FakeFs) -> Self {
174        Self {
175            env: Arc::new(env),
176            fs: Arc::new(fs),
177        }
178    }
179
180    /// A platform where nothing exists: no variables, no files.
181    pub(crate) fn blank() -> Self {
182        Self::fake(FakeEnv::empty(), FakeFs::empty())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn fake_env_returns_only_what_it_was_given() {
192        let env = FakeEnv::from([("A", "1"), ("B", "2")]);
193        assert_eq!(env.var("A"), Some(OsString::from("1")));
194        assert_eq!(env.var("MISSING"), None);
195
196        let mut names: Vec<String> = env.vars().into_iter().map(|(k, _)| k).collect();
197        names.sort();
198        assert_eq!(names, vec!["A", "B"]);
199    }
200
201    #[test]
202    fn fake_env_does_not_see_the_real_environment() {
203        // PATH is set in every real process, so it must not leak into the fake.
204        assert!(SystemEnv.var("PATH").is_some());
205        assert_eq!(FakeEnv::empty().var("PATH"), None);
206    }
207}