use crate::nixpkgs_problem::NixpkgsProblem; use crate::utils; use crate::utils::LineIndex; use crate::validation::{self, ResultIteratorExt, Validation::Success}; use anyhow::Context; use rnix::{Root, SyntaxKind::NODE_PATH}; use std::ffi::OsStr; use std::fs::read_to_string; use std::path::Path; /// Check that every package directory in pkgs/by-name doesn't link to outside that directory. /// Both symlinks and Nix path expressions are checked. pub fn check_references( relative_package_dir: &Path, absolute_package_dir: &Path, ) -> validation::Result<()> { // The empty argument here is the subpath under the package directory to check // An empty one means the package directory itself check_path(relative_package_dir, absolute_package_dir, Path::new("")).with_context(|| { format!( "While checking the references in package directory {}", relative_package_dir.display() ) }) } /// Checks for a specific path to not have references outside fn check_path( relative_package_dir: &Path, absolute_package_dir: &Path, subpath: &Path, ) -> validation::Result<()> { let path = absolute_package_dir.join(subpath); Ok(if path.is_symlink() { // Check whether the symlink resolves to outside the package directory match path.canonicalize() { Ok(target) => { // No need to handle the case of it being inside the directory, since we scan through the // entire directory recursively anyways if let Err(_prefix_error) = target.strip_prefix(absolute_package_dir) { NixpkgsProblem::OutsideSymlink { relative_package_dir: relative_package_dir.to_path_buf(), subpath: subpath.to_path_buf(), } .into() } else { Success(()) } } Err(io_error) => NixpkgsProblem::UnresolvableSymlink { relative_package_dir: relative_package_dir.to_path_buf(), subpath: subpath.to_path_buf(), io_error, } .into(), } } else if path.is_dir() { // Recursively check each entry validation::sequence_( utils::read_dir_sorted(&path)? .into_iter() .map(|entry| { let entry_subpath = subpath.join(entry.file_name()); check_path(relative_package_dir, absolute_package_dir, &entry_subpath) .with_context(|| { format!("Error while recursing into {}", subpath.display()) }) }) .collect_vec()?, ) } else if path.is_file() { // Only check Nix files if let Some(ext) = path.extension() { if ext == OsStr::new("nix") { check_nix_file(relative_package_dir, absolute_package_dir, subpath).with_context( || format!("Error while checking Nix file {}", subpath.display()), )? } else { Success(()) } } else { Success(()) } } else { // This should never happen, git doesn't support other file types anyhow::bail!("Unsupported file type for path {}", subpath.display()); }) } /// Check whether a nix file contains path expression references pointing outside the package /// directory fn check_nix_file( relative_package_dir: &Path, absolute_package_dir: &Path, subpath: &Path, ) -> validation::Result<()> { let path = absolute_package_dir.join(subpath); let parent_dir = path .parent() .with_context(|| format!("Could not get parent of path {}", subpath.display()))?; let contents = read_to_string(&path) .with_context(|| format!("Could not read file {}", subpath.display()))?; let root = Root::parse(&contents); if let Some(error) = root.errors().first() { // NOTE: There's now another Nixpkgs CI check to make sure all changed Nix files parse // correctly, though that uses mainline Nix instead of rnix, so it doesn't give the same // errors. In the future we should unify these two checks, ideally moving the other CI // check into this tool as well and checking for both mainline Nix and rnix. return Ok(NixpkgsProblem::CouldNotParseNix { relative_package_dir: relative_package_dir.to_path_buf(), subpath: subpath.to_path_buf(), error: error.clone(), } .into()); } let line_index = LineIndex::new(&contents); Ok(validation::sequence_(root.syntax().descendants().map( |node| { let text = node.text().to_string(); let line = line_index.line(node.text_range().start().into()); if node.kind() != NODE_PATH { // We're only interested in Path expressions Success(()) } else if node.children().count() != 0 { // Filters out ./foo/${bar}/baz // TODO: We can just check ./foo NixpkgsProblem::PathInterpolation { relative_package_dir: relative_package_dir.to_path_buf(), subpath: subpath.to_path_buf(), line, text, } .into() } else if text.starts_with('<') { // Filters out search paths like NixpkgsProblem::SearchPath { relative_package_dir: relative_package_dir.to_path_buf(), subpath: subpath.to_path_buf(), line, text, } .into() } else { // Resolves the reference of the Nix path // turning `../baz` inside `/foo/bar/default.nix` to `/foo/baz` match parent_dir.join(Path::new(&text)).canonicalize() { Ok(target) => { // Then checking if it's still in the package directory // No need to handle the case of it being inside the directory, since we scan through the // entire directory recursively anyways if let Err(_prefix_error) = target.strip_prefix(absolute_package_dir) { NixpkgsProblem::OutsidePathReference { relative_package_dir: relative_package_dir.to_path_buf(), subpath: subpath.to_path_buf(), line, text, } .into() } else { Success(()) } } Err(e) => NixpkgsProblem::UnresolvablePathReference { relative_package_dir: relative_package_dir.to_path_buf(), subpath: subpath.to_path_buf(), line, text, io_error: e, } .into(), } } }, ))) }