about summary refs log tree commit diff
path: root/tvix/eval
diff options
context:
space:
mode:
authorGriffin Smith <root@gws.fyi>2022-10-10T02·57-0400
committergrfn <grfn@gws.fyi>2022-10-10T20·23+0000
commit5e2b44b4161dba88dfd34f3cd649f592c304ae5b (patch)
tree072225a1ad25e4c5c40cc23a370b6d94a653faf8 /tvix/eval
parent0e9f5d6890df5820be836cff78622d3f1dcfe155 (diff)
feat(tvix/eval): Add a struct implementing NIX_PATH r/5086
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 <grfn@gws.fyi>
Reviewed-by: tazjin <tazjin@tvl.su>
Reviewed-by: Adam Joseph <adam@westernsemico.com>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix/eval')
-rw-r--r--tvix/eval/src/lib.rs1
-rw-r--r--tvix/eval/src/nix_path.rs207
2 files changed, 208 insertions, 0 deletions
diff --git a/tvix/eval/src/lib.rs b/tvix/eval/src/lib.rs
index b4ffd25854ce..3c4fe26cd478 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 000000000000..61e45c66d065
--- /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 `<subdir>` would resolve to `/example/subdir`, and a
+    /// Nix path literal `<subdir/grandchild>` 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 `<prefix/subdir>` would resolve to `/example/subdir`,
+    /// and a Nix path literal `<prefix/subdir/grandchild>` would resolve to
+    /// `/example/subdir/grandchild`
+    Prefix { prefix: PathBuf, path: PathBuf },
+}
+
+impl NixPathEntry {
+    fn resolve(&self, lookup_path: &Path) -> io::Result<Option<PathBuf>> {
+        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<Self, Self::Err> {
+        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<NixPathEntry>,
+}
+
+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<P>(&self, path: P) -> Result<PathBuf, ErrorKind>
+    where
+        P: AsRef<Path>,
+    {
+        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<Self, Self::Err> {
+        let entries = s
+            .split(':')
+            .map(|s| s.parse())
+            .collect::<Result<Vec<_>, _>>()?;
+        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());
+        }
+    }
+}