From 5e2b44b4161dba88dfd34f3cd649f592c304ae5b Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Sun, 9 Oct 2022 22:57:21 -0400 Subject: feat(tvix/eval): Add a struct implementing NIX_PATH Add a simple struct implementing both the string parsing and path resolution rules of Nix's `NIX_PATH` environment variable, for use in resolving `<...>`-style paths Change-Id: Ife75f39aa5c12928278d81fe428fbadc98bac5cc Reviewed-on: https://cl.tvl.fyi/c/depot/+/6917 Autosubmit: grfn Reviewed-by: tazjin Reviewed-by: Adam Joseph Tested-by: BuildkiteCI --- tvix/eval/src/lib.rs | 1 + tvix/eval/src/nix_path.rs | 207 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 tvix/eval/src/nix_path.rs (limited to 'tvix/eval/src') diff --git a/tvix/eval/src/lib.rs b/tvix/eval/src/lib.rs index b4ffd25854..3c4fe26cd4 100644 --- a/tvix/eval/src/lib.rs +++ b/tvix/eval/src/lib.rs @@ -12,6 +12,7 @@ mod value; mod vm; mod warnings; +mod nix_path; #[cfg(test)] mod properties; #[cfg(test)] diff --git a/tvix/eval/src/nix_path.rs b/tvix/eval/src/nix_path.rs new file mode 100644 index 0000000000..61e45c66d0 --- /dev/null +++ b/tvix/eval/src/nix_path.rs @@ -0,0 +1,207 @@ +use std::convert::Infallible; +use std::io; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use crate::errors::ErrorKind; + +#[derive(Debug, Clone, PartialEq, Eq)] +enum NixPathEntry { + /// Resolve subdirectories of this path within `<...>` brackets. This + /// corresponds to bare paths within the `NIX_PATH` environment variable + /// + /// For example, with `NixPathEntry::Path("/example")` and the following + /// directory structure: + /// + /// ```notrust + /// example + /// └── subdir + /// └── grandchild + /// ``` + /// + /// A Nix path literal `` would resolve to `/example/subdir`, and a + /// Nix path literal `` would resolve to + /// `/example/subdir/grandchild` + Path(PathBuf), + + /// Resolve paths starting with `prefix` as subdirectories of `path`. This + /// corresponds to `prefix=path` within the `NIX_PATH` environment variable. + /// + /// For example, with `NixPathEntry::Prefix { prefix: "prefix", path: + /// "/example" }` and the following directory structure: + /// + /// ```notrust + /// example + /// └── subdir + /// └── grandchild + /// ``` + /// + /// A Nix path literal `` would resolve to `/example/subdir`, + /// and a Nix path literal `` would resolve to + /// `/example/subdir/grandchild` + Prefix { prefix: PathBuf, path: PathBuf }, +} + +impl NixPathEntry { + fn resolve(&self, lookup_path: &Path) -> io::Result> { + let resolve_in = + |parent: &Path, lookup_path: &Path| match parent.join(lookup_path).canonicalize() { + Ok(path) => Ok(Some(path)), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(e), + }; + + match self { + NixPathEntry::Path(p) => resolve_in(p, lookup_path), + NixPathEntry::Prefix { prefix, path } => { + if let Ok(child_path) = lookup_path.strip_prefix(prefix) { + resolve_in(path, child_path) + } else { + Ok(None) + } + } + } + } +} + +impl FromStr for NixPathEntry { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + match s.split_once('=') { + Some((prefix, path)) => Ok(Self::Prefix { + prefix: prefix.into(), + path: path.into(), + }), + None => Ok(Self::Path(s.into())), + } + } +} + +/// Struct implementing the format and path resolution rules of the `NIX_PATH` +/// environment variable. +/// +/// This struct can be constructed by parsing a string using the [`FromStr`] +/// impl, or via [`str::parse`]. Nix `<...>` paths can then be resolved using +/// [`NixPath::resolve`]. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct NixPath { + entries: Vec, +} + +impl NixPath { + /// Attempt to resolve the given `path` within this [`NixPath`] using the + /// path resolution rules for `<...>`-style paths + #[allow(dead_code)] // TODO(grfn) + pub fn resolve

(&self, path: P) -> Result + where + P: AsRef, + { + let path = path.as_ref(); + for entry in &self.entries { + if let Some(p) = entry.resolve(path)? { + return Ok(p); + } + } + Err(ErrorKind::PathResolution(format!( + "path '{}' was not found in the Nix search path", + path.display() + ))) + } +} + +impl FromStr for NixPath { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + let entries = s + .split(':') + .map(|s| s.parse()) + .collect::, _>>()?; + Ok(NixPath { entries }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod parse { + use super::*; + + #[test] + fn bare_paths() { + assert_eq!( + NixPath::from_str("/foo/bar:/baz").unwrap(), + NixPath { + entries: vec![ + NixPathEntry::Path("/foo/bar".into()), + NixPathEntry::Path("/baz".into()) + ], + } + ); + } + + #[test] + fn mixed_prefix_and_paths() { + assert_eq!( + NixPath::from_str("nixpkgs=/my/nixpkgs:/etc/nixos").unwrap(), + NixPath { + entries: vec![ + NixPathEntry::Prefix { + prefix: "nixpkgs".into(), + path: "/my/nixpkgs".into() + }, + NixPathEntry::Path("/etc/nixos".into()) + ], + } + ); + } + } + + mod resolve { + use std::env::current_dir; + + use path_clean::PathClean; + + use super::*; + + #[test] + fn simple_dir() { + let nix_path = NixPath::from_str("./.").unwrap(); + let res = nix_path.resolve("src").unwrap(); + assert_eq!(res, current_dir().unwrap().join("src").clean()); + } + + #[test] + fn failed_resolution() { + let nix_path = NixPath::from_str("./.").unwrap(); + let err = nix_path.resolve("nope").unwrap_err(); + assert!( + matches!(err, ErrorKind::PathResolution(..)), + "err = {err:?}" + ); + } + + #[test] + fn second_in_path() { + let nix_path = NixPath::from_str("./.:/").unwrap(); + let res = nix_path.resolve("bin").unwrap(); + assert_eq!(res, Path::new("/bin")); + } + + #[test] + fn prefix() { + let nix_path = NixPath::from_str("/:tvix=.").unwrap(); + let res = nix_path.resolve("tvix/src").unwrap(); + assert_eq!(res, current_dir().unwrap().join("src").clean()); + } + + #[test] + fn matching_prefix() { + let nix_path = NixPath::from_str("/:tvix=.").unwrap(); + let res = nix_path.resolve("tvix").unwrap(); + assert_eq!(res, current_dir().unwrap().clean()); + } + } +} -- cgit 1.4.1