use std::path::{Path, PathBuf}; use itertools::Itertools; use tvix_castore::proto::{NamedNode, ValidateNodeError}; mod grpc_buildservice_wrapper; pub use grpc_buildservice_wrapper::GRPCBuildServiceWrapper; tonic::include_proto!("tvix.build.v1"); #[cfg(feature = "tonic-reflection")] /// Compiled file descriptors for implementing [gRPC /// reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) with e.g. /// [`tonic_reflection`](https://docs.rs/tonic-reflection). pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("tvix.build.v1"); /// Errors that occur during the validation of [BuildRequest] messages. #[derive(Debug, thiserror::Error)] pub enum ValidateBuildRequestError { #[error("invalid input node at position {0}: {1}")] InvalidInputNode(usize, ValidateNodeError), #[error("input nodes are not sorted by name")] InputNodesNotSorted, #[error("invalid working_dir")] InvalidWorkingDir, #[error("scratch_paths not sorted")] ScratchPathsNotSorted, #[error("invalid scratch path at position {0}")] InvalidScratchPath(usize), #[error("invalid inputs_dir")] InvalidInputsDir, #[error("invalid output path at position {0}")] InvalidOutputPath(usize), #[error("outputs not sorted")] OutputsNotSorted, #[error("invalid environment variable at position {0}")] InvalidEnvVar(usize), #[error("EnvVar not sorted by their keys")] EnvVarNotSorted, #[error("invalid build constraints: {0}")] InvalidBuildConstraints(ValidateBuildConstraintsError), #[error("invalid additional file path at position: {0}")] InvalidAdditionalFilePath(usize), #[error("additional_files not sorted")] AdditionalFilesNotSorted, } /// Checks a path to be without any '..' components, and clean (no superfluous /// slashes). fn is_clean_path<P: AsRef<Path>>(p: P) -> bool { let p = p.as_ref(); // Look at all components, bail in case of ".", ".." and empty normal // segments (superfluous slashes) // We still need to assemble a cleaned PathBuf, and compare the OsString // later, as .components() already does do some normalization before // yielding. let mut cleaned_p = PathBuf::new(); for component in p.components() { match component { std::path::Component::Prefix(_) => {} std::path::Component::RootDir => {} std::path::Component::CurDir => return false, std::path::Component::ParentDir => return false, std::path::Component::Normal(a) => { if a.is_empty() { return false; } } } cleaned_p.push(component); } // if cleaned_p looks like p, we're good. if cleaned_p.as_os_str() != p.as_os_str() { return false; } true } fn is_clean_relative_path<P: AsRef<Path>>(p: P) -> bool { if p.as_ref().is_absolute() { return false; } is_clean_path(p) } fn is_clean_absolute_path<P: AsRef<Path>>(p: P) -> bool { if !p.as_ref().is_absolute() { return false; } is_clean_path(p) } /// Checks if a given list is sorted. fn is_sorted<I>(data: I) -> bool where I: Iterator, I::Item: Ord + Clone, { data.tuple_windows().all(|(a, b)| a <= b) } impl BuildRequest { /// Ensures the build request is valid. /// This means, all input nodes need to be valid, paths in lists need to be sorted, /// and all restrictions around paths themselves (relative, clean, …) need // to be fulfilled. pub fn validate(&self) -> Result<(), ValidateBuildRequestError> { // validate all input nodes for (i, n) in self.inputs.iter().enumerate() { // ensure the input node itself is valid n.validate() .map_err(|e| ValidateBuildRequestError::InvalidInputNode(i, e))?; } // now we can look at the names, and make sure they're sorted. if !is_sorted( self.inputs .iter() .map(|e| e.node.as_ref().unwrap().get_name()), ) { Err(ValidateBuildRequestError::InputNodesNotSorted)? } // validate working_dir if !is_clean_relative_path(&self.working_dir) { Err(ValidateBuildRequestError::InvalidWorkingDir)?; } // validate scratch paths for (i, p) in self.scratch_paths.iter().enumerate() { if !is_clean_relative_path(p) { Err(ValidateBuildRequestError::InvalidScratchPath(i))? } } if !is_sorted(self.scratch_paths.iter().map(|e| e.as_bytes())) { Err(ValidateBuildRequestError::ScratchPathsNotSorted)?; } // validate inputs_dir if !is_clean_relative_path(&self.inputs_dir) { Err(ValidateBuildRequestError::InvalidInputsDir)?; } // validate outputs for (i, p) in self.outputs.iter().enumerate() { if !is_clean_relative_path(p) { Err(ValidateBuildRequestError::InvalidOutputPath(i))? } } if !is_sorted(self.outputs.iter().map(|e| e.as_bytes())) { Err(ValidateBuildRequestError::OutputsNotSorted)?; } // validate environment_vars. for (i, e) in self.environment_vars.iter().enumerate() { if e.key.is_empty() || e.key.contains('=') { Err(ValidateBuildRequestError::InvalidEnvVar(i))? } } if !is_sorted(self.environment_vars.iter().map(|e| e.key.as_bytes())) { Err(ValidateBuildRequestError::EnvVarNotSorted)?; } // validate build constraints if let Some(constraints) = self.constraints.as_ref() { constraints .validate() .map_err(ValidateBuildRequestError::InvalidBuildConstraints)?; } // validate additional_files for (i, additional_file) in self.additional_files.iter().enumerate() { if !is_clean_relative_path(&additional_file.path) { Err(ValidateBuildRequestError::InvalidAdditionalFilePath(i))? } } if !is_sorted(self.additional_files.iter().map(|e| e.path.as_bytes())) { Err(ValidateBuildRequestError::AdditionalFilesNotSorted)?; } Ok(()) } } /// Errors that occur during the validation of /// [build_request::BuildConstraints] messages. #[derive(Debug, thiserror::Error)] pub enum ValidateBuildConstraintsError { #[error("invalid system")] InvalidSystem, #[error("invalid available_ro_paths at position {0}")] InvalidAvailableRoPaths(usize), #[error("available_ro_paths not sorted")] AvailableRoPathsNotSorted, } impl build_request::BuildConstraints { pub fn validate(&self) -> Result<(), ValidateBuildConstraintsError> { // validate system if self.system.is_empty() { Err(ValidateBuildConstraintsError::InvalidSystem)?; } // validate available_ro_paths for (i, p) in self.available_ro_paths.iter().enumerate() { if !is_clean_absolute_path(p) { Err(ValidateBuildConstraintsError::InvalidAvailableRoPaths(i))? } } if !is_sorted(self.available_ro_paths.iter().map(|e| e.as_bytes())) { Err(ValidateBuildConstraintsError::AvailableRoPathsNotSorted)?; } Ok(()) } } #[cfg(test)] mod tests { use test_case::test_case; use crate::proto::is_clean_relative_path; use super::is_clean_path; #[test_case("foo/bar/", false; "fail trailing slash")] #[test_case("foo/../bar", false; "fail dotdot")] #[test_case("foo/./bar", false; "fail singledot")] #[test_case("foo//bar", false; "fail unnecessary slashes")] #[test_case("//foo/bar", false; "fail absolute unnecessary slashes")] #[test_case("", true; "ok empty")] #[test_case("foo/bar", true; "ok relative")] #[test_case("/", true; "ok absolute")] #[test_case("/foo/bar", true; "ok absolute2")] fn test_is_clean_path(s: &str, expected: bool) { assert_eq!(is_clean_path(s), expected); } #[test_case("/", false; "fail absolute")] #[test_case("foo/bar", true; "ok relative")] fn test_is_clean_relative_path(s: &str, expected: bool) { assert_eq!(is_clean_relative_path(s), expected); } // TODO: add tests for BuildRequest validation itself }