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}