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}