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