tvix_eval/
nix_search_path.rs

1use path_clean::PathClean;
2use std::convert::Infallible;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5
6use crate::errors::{CatchableErrorKind, ErrorKind};
7use crate::EvalIO;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum NixSearchPathEntry {
11    /// Resolve subdirectories of this path within `<...>` brackets. This
12    /// corresponds to bare paths within the `NIX_PATH` environment variable
13    ///
14    /// For example, with `NixSearchPathEntry::Path("/example")` and the following
15    /// directory structure:
16    ///
17    /// ```notrust
18    /// example
19    /// └── subdir
20    ///     └── grandchild
21    /// ```
22    ///
23    /// A Nix path literal `<subdir>` would resolve to `/example/subdir`, and a
24    /// Nix path literal `<subdir/grandchild>` would resolve to
25    /// `/example/subdir/grandchild`
26    Path(PathBuf),
27
28    /// Resolve paths starting with `prefix` as subdirectories of `path`. This
29    /// corresponds to `prefix=path` within the `NIX_PATH` environment variable.
30    ///
31    /// For example, with `NixSearchPathEntry::Prefix { prefix: "prefix", path:
32    /// "/example" }` and the following directory structure:
33    ///
34    /// ```notrust
35    /// example
36    /// └── subdir
37    ///     └── grandchild
38    /// ```
39    ///
40    /// A Nix path literal `<prefix/subdir>` would resolve to `/example/subdir`,
41    /// and a Nix path literal `<prefix/subdir/grandchild>` would resolve to
42    /// `/example/subdir/grandchild`
43    Prefix { prefix: PathBuf, path: PathBuf },
44}
45
46fn canonicalise(path: PathBuf) -> Result<PathBuf, ErrorKind> {
47    let absolute = if path.is_absolute() {
48        path
49    } else {
50        // TODO(tazjin): probably panics in wasm?
51        std::env::current_dir()
52            .map_err(|e| ErrorKind::IO {
53                path: Some(path.clone()),
54                error: e.into(),
55            })?
56            .join(path)
57    }
58    .clean();
59
60    Ok(absolute)
61}
62
63impl NixSearchPathEntry {
64    /// Determine whether this path entry matches the given lookup path.
65    ///
66    /// For bare paths, an entry is considered to match if a matching
67    /// file exists under it.
68    ///
69    /// For prefixed path, an entry matches if the prefix does.
70    // TODO(tazjin): verify these rules in the C++ impl, seems fishy.
71    fn resolve<IO>(&self, io: IO, lookup_path: &Path) -> Result<Option<PathBuf>, ErrorKind>
72    where
73        IO: AsRef<dyn EvalIO>,
74    {
75        let path = match self {
76            NixSearchPathEntry::Path(parent) => canonicalise(parent.join(lookup_path))?,
77
78            NixSearchPathEntry::Prefix { prefix, path } => {
79                if let Ok(child_path) = lookup_path.strip_prefix(prefix) {
80                    canonicalise(path.join(child_path))?
81                } else {
82                    return Ok(None);
83                }
84            }
85        };
86
87        if io.as_ref().path_exists(&path).map_err(|e| ErrorKind::IO {
88            path: Some(path.clone()),
89            error: e.into(),
90        })? {
91            Ok(Some(path))
92        } else {
93            Ok(None)
94        }
95    }
96
97    pub fn get_path(&self) -> &Path {
98        match self {
99            Self::Prefix { path, .. } => path,
100            Self::Path(path) => path,
101        }
102    }
103}
104
105impl FromStr for NixSearchPathEntry {
106    type Err = Infallible;
107
108    fn from_str(s: &str) -> Result<Self, Self::Err> {
109        match s.split_once('=') {
110            Some((prefix, path)) => Ok(Self::Prefix {
111                prefix: prefix.into(),
112                path: path.into(),
113            }),
114            None => Ok(Self::Path(s.into())),
115        }
116    }
117}
118
119/// Struct implementing the format and path resolution rules of the `NIX_PATH`
120/// environment variable.
121///
122/// This struct can be constructed by parsing a string using the [`FromStr`]
123/// impl, or via [`str::parse`]. Nix `<...>` paths can then be resolved using
124/// [`NixSearchPath::resolve`].
125#[derive(Default, Debug, Clone, PartialEq, Eq)]
126pub struct NixSearchPath {
127    entries: Vec<NixSearchPathEntry>,
128}
129
130impl NixSearchPath {
131    /// Attempt to resolve the given `path` within this [`NixSearchPath`] using the
132    /// path resolution rules for `<...>`-style paths
133    pub fn resolve<P, IO>(
134        &self,
135        io: IO,
136        path: P,
137    ) -> Result<Result<PathBuf, CatchableErrorKind>, ErrorKind>
138    where
139        P: AsRef<Path>,
140        IO: AsRef<dyn EvalIO>,
141    {
142        let path = path.as_ref();
143        for entry in &self.entries {
144            if let Some(p) = entry.resolve(&io, path)? {
145                return Ok(Ok(p));
146            }
147        }
148        Ok(Err(CatchableErrorKind::NixPathResolution(
149            format!(
150                "path '{}' was not found in the Nix search path",
151                path.display()
152            )
153            .into_boxed_str(),
154        )))
155    }
156
157    pub fn get_entries(&self) -> &[NixSearchPathEntry] {
158        &self.entries
159    }
160}
161
162impl FromStr for NixSearchPath {
163    type Err = Infallible;
164
165    fn from_str(s: &str) -> Result<Self, Self::Err> {
166        let entries = s
167            .split(':')
168            .map(|s| s.parse())
169            .collect::<Result<Vec<_>, _>>()?;
170        Ok(NixSearchPath { entries })
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use std::rc::Rc;
178
179    mod parse {
180        use super::*;
181
182        #[test]
183        fn bare_paths() {
184            assert_eq!(
185                NixSearchPath::from_str("/foo/bar:/baz").unwrap(),
186                NixSearchPath {
187                    entries: vec![
188                        NixSearchPathEntry::Path("/foo/bar".into()),
189                        NixSearchPathEntry::Path("/baz".into())
190                    ],
191                }
192            );
193        }
194
195        #[test]
196        fn mixed_prefix_and_paths() {
197            assert_eq!(
198                NixSearchPath::from_str("nixpkgs=/my/nixpkgs:/etc/nixos").unwrap(),
199                NixSearchPath {
200                    entries: vec![
201                        NixSearchPathEntry::Prefix {
202                            prefix: "nixpkgs".into(),
203                            path: "/my/nixpkgs".into()
204                        },
205                        NixSearchPathEntry::Path("/etc/nixos".into())
206                    ],
207                }
208            );
209        }
210    }
211
212    // this uses StdIO, which is only available with the impure feature.
213    #[cfg(feature = "impure")]
214    mod resolve {
215        use crate::StdIO;
216        use path_clean::PathClean;
217        use std::env::current_dir;
218
219        use super::*;
220
221        #[test]
222        fn simple_dir() {
223            let nix_search_path = NixSearchPath::from_str("./.").unwrap();
224            let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
225            let res = nix_search_path.resolve(&io, "src").unwrap();
226            assert_eq!(
227                res.unwrap().to_path_buf(),
228                current_dir().unwrap().join("src").clean()
229            );
230        }
231
232        #[test]
233        fn failed_resolution() {
234            let nix_search_path = NixSearchPath::from_str("./.").unwrap();
235            let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
236            let err = nix_search_path.resolve(&io, "nope").unwrap();
237            assert!(
238                matches!(err, Err(CatchableErrorKind::NixPathResolution(..))),
239                "err = {err:?}"
240            );
241        }
242
243        #[test]
244        fn second_in_path() {
245            let nix_search_path = NixSearchPath::from_str("./.:/").unwrap();
246            let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
247            let res = nix_search_path.resolve(&io, "etc").unwrap();
248            assert_eq!(res.unwrap().to_path_buf(), Path::new("/etc"));
249        }
250
251        #[test]
252        fn prefix() {
253            let nix_search_path = NixSearchPath::from_str("/:tvix=.").unwrap();
254            let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
255            let res = nix_search_path.resolve(&io, "tvix/src").unwrap();
256            assert_eq!(
257                res.unwrap().to_path_buf(),
258                current_dir().unwrap().join("src").clean()
259            );
260        }
261
262        #[test]
263        fn matching_prefix() {
264            let nix_search_path = NixSearchPath::from_str("/:tvix=.").unwrap();
265            let io = Rc::new(StdIO {}) as Rc<dyn EvalIO>;
266            let res = nix_search_path.resolve(&io, "tvix").unwrap();
267            assert_eq!(res.unwrap().to_path_buf(), current_dir().unwrap().clean());
268        }
269    }
270}