snix_eval/
io.rs

1//! Interface for injecting I/O-related functionality into snix-eval.
2//!
3//! The Nix language contains several builtins (e.g. `builtins.readDir`), as
4//! well as language feature (e.g. string-"coercion" of paths) that interact
5//! with the filesystem.
6//!
7//! The language evaluator implemented by this crate does not depend on any
8//! particular filesystem interaction model. Instead, this module provides a
9//! trait that can be implemented by snix-eval callers to provide the
10//! functionality they desire.
11//!
12//! In theory this can be used to implement "mocked" filesystem interactions, or
13//! interaction with remote filesystems, etc.
14//!
15//! In the context of Nix builds, callers also use this interface to determine
16//! how store paths are opened and so on.
17
18use std::{
19    env,
20    ffi::{OsStr, OsString},
21    io,
22    path::{Path, PathBuf},
23};
24
25#[cfg(all(target_family = "unix", feature = "impure"))]
26use std::os::unix::ffi::OsStringExt;
27
28#[cfg(feature = "impure")]
29use std::fs::File;
30
31/// Types of files as represented by `builtins.readFileType` and `builtins.readDir` in Nix.
32#[derive(Debug)]
33pub enum FileType {
34    Directory,
35    Regular,
36    Symlink,
37    Unknown,
38}
39
40impl std::fmt::Display for FileType {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        let type_as_str = match &self {
43            FileType::Directory => "directory",
44            FileType::Regular => "regular",
45            FileType::Symlink => "symlink",
46            FileType::Unknown => "unknown",
47        };
48
49        write!(f, "{type_as_str}")
50    }
51}
52
53impl From<std::fs::FileType> for FileType {
54    fn from(value: std::fs::FileType) -> Self {
55        if value.is_file() {
56            Self::Regular
57        } else if value.is_dir() {
58            Self::Directory
59        } else if value.is_symlink() {
60            Self::Symlink
61        } else {
62            Self::Unknown
63        }
64    }
65}
66
67/// Represents all possible filesystem interactions that exist in the Nix
68/// language, and that need to be executed somehow.
69///
70/// This trait is specifically *only* concerned with what is visible on the
71/// level of the language. All internal implementation details are not part of
72/// this trait.
73pub trait EvalIO {
74    /// Verify whether the file at the specified path exists.
75    ///
76    /// This is used for the following language evaluation cases:
77    ///
78    /// * checking whether a file added to the `NIX_PATH` actually exists when
79    ///   it is referenced in `<...>` brackets.
80    /// * `builtins.pathExists :: path -> bool`
81    fn path_exists(&self, path: &Path) -> io::Result<bool>;
82
83    /// Open the file at the specified path to a `io::Read`.
84    fn open(&self, path: &Path) -> io::Result<Box<dyn io::Read>>;
85
86    /// Return the [FileType] of the given path, or an error if it doesn't
87    /// exist.
88    fn file_type(&self, path: &Path) -> io::Result<FileType>;
89
90    /// Read the directory at the specified path and return the names
91    /// of its entries associated with their [`FileType`].
92    ///
93    /// This is used for the following language evaluation cases:
94    ///
95    /// * `builtins.readDir :: path -> attrs<filename, filetype>`
96    fn read_dir(&self, path: &Path) -> io::Result<Vec<(bytes::Bytes, FileType)>>;
97
98    /// Import the given path. What this means depends on the implementation,
99    /// for example for a `std::io`-based implementation this might be a no-op,
100    /// while for a Snix store this might be a copy of the given files to the
101    /// store.
102    ///
103    /// This is used for the following language evaluation cases:
104    ///
105    /// * string coercion of path literals (e.g. `/foo/bar`), which are expected
106    ///   to return a path
107    /// * `builtins.toJSON` on a path literal, also expected to return a path
108    fn import_path(&self, path: &Path) -> io::Result<PathBuf>;
109
110    /// Returns the root of the store directory, if such a thing
111    /// exists in the evaluation context.
112    ///
113    /// This is used for the following language evaluation cases:
114    ///
115    /// * `builtins.storeDir :: string`
116    fn store_dir(&self) -> Option<String> {
117        None
118    }
119
120    /// Fetches the environment variable key from the current process.
121    fn get_env(&self, key: &OsStr) -> Option<OsString>;
122}
123
124/// Implementation of [`EvalIO`] that simply uses the equivalent
125/// standard library functions, i.e. does local file-IO.
126#[cfg(feature = "impure")]
127pub struct StdIO;
128
129// TODO: we might want to make this whole impl to be target_family = "unix".
130#[cfg(feature = "impure")]
131impl EvalIO for StdIO {
132    fn path_exists(&self, path: &Path) -> io::Result<bool> {
133        // In general, an IO error indicates the path doesn't exist
134        Ok(path.try_exists().unwrap_or(false))
135    }
136
137    fn open(&self, path: &Path) -> io::Result<Box<dyn io::Read>> {
138        Ok(Box::new(File::open(path)?))
139    }
140
141    fn file_type(&self, path: &Path) -> io::Result<FileType> {
142        let file_type = std::fs::symlink_metadata(path)?;
143
144        Ok(if file_type.is_dir() {
145            FileType::Directory
146        } else if file_type.is_file() {
147            FileType::Regular
148        } else if file_type.is_symlink() {
149            FileType::Symlink
150        } else {
151            FileType::Unknown
152        })
153    }
154
155    fn read_dir(&self, path: &Path) -> io::Result<Vec<(bytes::Bytes, FileType)>> {
156        let mut result = vec![];
157
158        for entry in path.read_dir()? {
159            let entry = entry?;
160            // Use entry.file_type() instead of entry.metadata() to avoid stat syscalls.
161            // file_type() uses the d_type field from the readdir() syscall (when available),
162            // which is cached in the DirEntry and doesn't require additional filesystem access.
163            let file_type = entry.file_type()?;
164
165            let val = if file_type.is_dir() {
166                FileType::Directory
167            } else if file_type.is_file() {
168                FileType::Regular
169            } else if file_type.is_symlink() {
170                FileType::Symlink
171            } else {
172                FileType::Unknown
173            };
174
175            result.push((entry.file_name().into_vec().into(), val))
176        }
177
178        Ok(result)
179    }
180
181    // this is a no-op for `std::io`, as the user can already refer to
182    // the path directly
183    fn import_path(&self, path: &Path) -> io::Result<PathBuf> {
184        Ok(path.to_path_buf())
185    }
186
187    fn get_env(&self, key: &OsStr) -> Option<OsString> {
188        env::var_os(key)
189    }
190}
191
192/// Dummy implementation of [`EvalIO`], can be used in contexts where
193/// IO is not available but code should "pretend" that it is.
194pub struct DummyIO;
195
196impl EvalIO for DummyIO {
197    fn path_exists(&self, _: &Path) -> io::Result<bool> {
198        Err(io::Error::new(
199            io::ErrorKind::Unsupported,
200            "I/O methods are not implemented in DummyIO",
201        ))
202    }
203
204    fn open(&self, _: &Path) -> io::Result<Box<dyn io::Read>> {
205        Err(io::Error::new(
206            io::ErrorKind::Unsupported,
207            "I/O methods are not implemented in DummyIO",
208        ))
209    }
210
211    fn file_type(&self, _: &Path) -> io::Result<FileType> {
212        Err(io::Error::new(
213            io::ErrorKind::Unsupported,
214            "I/O methods are not implemented in DummyIO",
215        ))
216    }
217
218    fn read_dir(&self, _: &Path) -> io::Result<Vec<(bytes::Bytes, FileType)>> {
219        Err(io::Error::new(
220            io::ErrorKind::Unsupported,
221            "I/O methods are not implemented in DummyIO",
222        ))
223    }
224
225    fn import_path(&self, _: &Path) -> io::Result<PathBuf> {
226        Err(io::Error::new(
227            io::ErrorKind::Unsupported,
228            "I/O methods are not implemented in DummyIO",
229        ))
230    }
231
232    fn get_env(&self, _: &OsStr) -> Option<OsString> {
233        None
234    }
235}