diff options
Diffstat (limited to 'tvix/build/src/proto/mod.rs')
-rw-r--r-- | tvix/build/src/proto/mod.rs | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/tvix/build/src/proto/mod.rs b/tvix/build/src/proto/mod.rs new file mode 100644 index 000000000000..b36049d05b9d --- /dev/null +++ b/tvix/build/src/proto/mod.rs @@ -0,0 +1,265 @@ +use std::path::{Path, PathBuf}; + +use itertools::Itertools; +use tvix_castore::DirectoryError; + +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, DirectoryError), + + #[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 names. Make sure they're sorted + + let mut last_name: bytes::Bytes = "".into(); + for (i, node) in self.inputs.iter().enumerate() { + // TODO(flokli): store result somewhere + let (name, _node) = node + .clone() + .into_name_and_node() + .map_err(|e| ValidateBuildRequestError::InvalidInputNode(i, e))?; + + if name.as_ref() <= last_name.as_ref() { + return Err(ValidateBuildRequestError::InputNodesNotSorted); + } else { + last_name = name.into() + } + } + + // 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 super::{is_clean_path, is_clean_relative_path}; + use rstest::rstest; + + #[rstest] + #[case::fail_trailing_slash("foo/bar/", false)] + #[case::fail_dotdot("foo/../bar", false)] + #[case::fail_singledot("foo/./bar", false)] + #[case::fail_unnecessary_slashes("foo//bar", false)] + #[case::fail_absolute_unnecessary_slashes("//foo/bar", false)] + #[case::ok_empty("", true)] + #[case::ok_relative("foo/bar", true)] + #[case::ok_absolute("/", true)] + #[case::ok_absolute2("/foo/bar", true)] + fn test_is_clean_path(#[case] s: &str, #[case] expected: bool) { + assert_eq!(is_clean_path(s), expected); + } + + #[rstest] + #[case::fail_absolute("/", false)] + #[case::ok_relative("foo/bar", true)] + fn test_is_clean_relative_path(#[case] s: &str, #[case] expected: bool) { + assert_eq!(is_clean_relative_path(s), expected); + } + + // TODO: add tests for BuildRequest validation itself +} |