snix_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)]
10enum 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
98impl FromStr for NixSearchPathEntry {
99    type Err = Infallible;
100
101    fn from_str(s: &str) -> Result<Self, Self::Err> {
102        match s.split_once('=') {
103            Some((prefix, path)) => Ok(Self::Prefix {
104                prefix: prefix.into(),
105                path: path.into(),
106            }),
107            None => Ok(Self::Path(s.into())),
108        }
109    }
110}
111
112/// Struct implementing the format and path resolution rules of the `NIX_PATH`
113/// environment variable.
114///
115/// This struct can be constructed by parsing a string using the [`FromStr`]
116/// impl, or via [`str::parse`]. Nix `<...>` paths can then be resolved using
117/// [`NixSearchPath::resolve`].
118#[derive(Default, Debug, Clone, PartialEq, Eq)]
119pub struct NixSearchPath {
120    entries: Vec<NixSearchPathEntry>,
121}
122
123impl NixSearchPath {
124    /// Attempt to resolve the given `path` within this [`NixSearchPath`] using the
125    /// path resolution rules for `<...>`-style paths
126    pub fn resolve<P, IO>(
127        &self,
128        io: IO,
129        path: P,
130    ) -> Result<Result<PathBuf, CatchableErrorKind>, ErrorKind>
131    where
132        P: AsRef<Path>,
133        IO: AsRef<dyn EvalIO>,
134    {
135        let path = path.as_ref();
136        for entry in &self.entries {
137            if let Some(p) = entry.resolve(&io, path)? {
138                return Ok(Ok(p));
139            }
140        }
141        Ok(Err(CatchableErrorKind::NixPathResolution(
142            format!(
143                "path '{}' was not found in the Nix search path",
144                path.display()
145            )
146            .into_boxed_str(),
147        )))
148    }
149}
150
151impl FromStr for NixSearchPath {
152    type Err = Infallible;
153
154    fn from_str(s: &str) -> Result<Self, Self::Err> {
155        let entries = s
156            .split(':')
157            .map(|s| s.parse())
158            .collect::<Result<Vec<_>, _>>()?;
159        Ok(NixSearchPath { entries })
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    mod parse {
168        use super::*;
169
170        #[test]
171        fn bare_paths() {
172            assert_eq!(
173                NixSearchPath::from_str("/foo/bar:/baz").unwrap(),
174                NixSearchPath {
175                    entries: vec![
176                        NixSearchPathEntry::Path("/foo/bar".into()),
177                        NixSearchPathEntry::Path("/baz".into())
178                    ],
179                }
180            );
181        }
182
183        #[test]
184        fn mixed_prefix_and_paths() {
185            assert_eq!(
186                NixSearchPath::from_str("nixpkgs=/my/nixpkgs:/etc/nixos").unwrap(),
187                NixSearchPath {
188                    entries: vec![
189                        NixSearchPathEntry::Prefix {
190                            prefix: "nixpkgs".into(),
191                            path: "/my/nixpkgs".into()
192                        },
193                        NixSearchPathEntry::Path("/etc/nixos".into())
194                    ],
195                }
196            );
197        }
198    }
199
200    // this uses StdIO, which is only available with the impure feature.
201    #[cfg(feature = "impure")]
202    mod resolve {
203        use crate::StdIO;
204        use path_clean::PathClean;
205        use std::env::current_dir;
206
207        use super::*;
208
209        #[test]
210        fn simple_dir() {
211            let nix_search_path = NixSearchPath::from_str("./.").unwrap();
212            let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
213            let res = nix_search_path.resolve(&io, "src").unwrap();
214            assert_eq!(
215                res.unwrap().to_path_buf(),
216                current_dir().unwrap().join("src").clean()
217            );
218        }
219
220        #[test]
221        fn failed_resolution() {
222            let nix_search_path = NixSearchPath::from_str("./.").unwrap();
223            let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
224            let err = nix_search_path.resolve(&io, "nope").unwrap();
225            assert!(
226                matches!(err, Err(CatchableErrorKind::NixPathResolution(..))),
227                "err = {err:?}"
228            );
229        }
230
231        #[test]
232        fn second_in_path() {
233            let nix_search_path = NixSearchPath::from_str("./.:/").unwrap();
234            let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
235            let res = nix_search_path.resolve(&io, "etc").unwrap();
236            assert_eq!(res.unwrap().to_path_buf(), Path::new("/etc"));
237        }
238
239        #[test]
240        fn prefix() {
241            let nix_search_path = NixSearchPath::from_str("/:snix=.").unwrap();
242            let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
243            let res = nix_search_path.resolve(&io, "snix/src").unwrap();
244            assert_eq!(
245                res.unwrap().to_path_buf(),
246                current_dir().unwrap().join("src").clean()
247            );
248        }
249
250        #[test]
251        fn matching_prefix() {
252            let nix_search_path = NixSearchPath::from_str("/:snix=.").unwrap();
253            let io = Box::new(StdIO {}) as Box<dyn EvalIO>;
254            let res = nix_search_path.resolve(&io, "snix").unwrap();
255            assert_eq!(res.unwrap().to_path_buf(), current_dir().unwrap().clean());
256        }
257    }
258}