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