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}