snix_eval/
nix_search_path.rs1use 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 Path(PathBuf),
27
28 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 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 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#[derive(Default, Debug, Clone, PartialEq, Eq)]
119pub struct NixSearchPath {
120 entries: Vec<NixSearchPathEntry>,
121}
122
123impl NixSearchPath {
124 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 #[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}