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")]
pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("tvix.build.v1");
#[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,
}
fn is_clean_path<P: AsRef<Path>>(p: P) -> bool {
let p = p.as_ref();
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.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)
}
fn is_sorted<I>(data: I) -> bool
where
I: Iterator,
I::Item: Ord + Clone,
{
data.tuple_windows().all(|(a, b)| a <= b)
}
impl BuildRequest {
pub fn validate(&self) -> Result<(), ValidateBuildRequestError> {
let mut last_name: bytes::Bytes = "".into();
for (i, node) in self.inputs.iter().enumerate() {
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()
}
}
if !is_clean_relative_path(&self.working_dir) {
Err(ValidateBuildRequestError::InvalidWorkingDir)?;
}
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)?;
}
if !is_clean_relative_path(&self.inputs_dir) {
Err(ValidateBuildRequestError::InvalidInputsDir)?;
}
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)?;
}
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)?;
}
if let Some(constraints) = self.constraints.as_ref() {
constraints
.validate()
.map_err(ValidateBuildRequestError::InvalidBuildConstraints)?;
}
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(())
}
}
#[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> {
if self.system.is_empty() {
Err(ValidateBuildConstraintsError::InvalidSystem)?;
}
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);
}
}