use prost::Message; use std::cmp::Ordering; mod grpc_blobservice_wrapper; mod grpc_directoryservice_wrapper; use crate::{path::PathComponent, B3Digest, DirectoryError}; pub use grpc_blobservice_wrapper::GRPCBlobServiceWrapper; pub use grpc_directoryservice_wrapper::GRPCDirectoryServiceWrapper; tonic::include_proto!("tvix.castore.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.castore.v1"); #[cfg(test)] mod tests; /// Errors that occur during StatBlobResponse validation #[derive(Debug, PartialEq, Eq, thiserror::Error)] pub enum ValidateStatBlobResponseError { /// Invalid digest length encountered #[error("Invalid digest length {0} for chunk #{1}")] InvalidDigestLen(usize, usize), } fn checked_sum(iter: impl IntoIterator<Item = u64>) -> Option<u64> { iter.into_iter().try_fold(0u64, |acc, i| acc.checked_add(i)) } impl Directory { /// The size of a directory is the number of all regular and symlink elements, /// the number of directory elements, and their size fields. pub fn size(&self) -> u64 { if cfg!(debug_assertions) { self.size_checked() .expect("Directory::size exceeds u64::MAX") } else { self.size_checked().unwrap_or(u64::MAX) } } fn size_checked(&self) -> Option<u64> { checked_sum([ self.files.len().try_into().ok()?, self.symlinks.len().try_into().ok()?, self.directories.len().try_into().ok()?, checked_sum(self.directories.iter().map(|e| e.size))?, ]) } /// Calculates the digest of a Directory, which is the blake3 hash of a /// Directory protobuf message, serialized in protobuf canonical form. pub fn digest(&self) -> B3Digest { let mut hasher = blake3::Hasher::new(); hasher .update(&self.encode_to_vec()) .finalize() .as_bytes() .into() } } impl TryFrom<Directory> for crate::Directory { type Error = DirectoryError; fn try_from(value: Directory) -> Result<Self, Self::Error> { // Check directories, files and symlinks are sorted // We'll notice duplicates across all three fields when constructing the Directory. // FUTUREWORK: use is_sorted() once stable, and/or implement the producer for // [crate::Directory::try_from_iter] iterating over all three and doing all checks inline. value .directories .iter() .try_fold(&b""[..], |prev_name, e| { match e.name.as_ref().cmp(prev_name) { Ordering::Less => Err(DirectoryError::WrongSorting(e.name.to_owned())), Ordering::Equal => Err(DirectoryError::DuplicateName( e.name .to_owned() .try_into() .map_err(DirectoryError::InvalidName)?, )), Ordering::Greater => Ok(e.name.as_ref()), } })?; value.files.iter().try_fold(&b""[..], |prev_name, e| { match e.name.as_ref().cmp(prev_name) { Ordering::Less => Err(DirectoryError::WrongSorting(e.name.to_owned())), Ordering::Equal => Err(DirectoryError::DuplicateName( e.name .to_owned() .try_into() .map_err(DirectoryError::InvalidName)?, )), Ordering::Greater => Ok(e.name.as_ref()), } })?; value.symlinks.iter().try_fold(&b""[..], |prev_name, e| { match e.name.as_ref().cmp(prev_name) { Ordering::Less => Err(DirectoryError::WrongSorting(e.name.to_owned())), Ordering::Equal => Err(DirectoryError::DuplicateName( e.name .to_owned() .try_into() .map_err(DirectoryError::InvalidName)?, )), Ordering::Greater => Ok(e.name.as_ref()), } })?; // FUTUREWORK: use is_sorted() once stable, and/or implement the producer for // [crate::Directory::try_from_iter] iterating over all three and doing all checks inline. let mut elems: Vec<(PathComponent, crate::Node)> = Vec::with_capacity(value.directories.len() + value.files.len() + value.symlinks.len()); for e in value.directories { elems.push( Node { node: Some(node::Node::Directory(e)), } .try_into_name_and_node()?, ); } for e in value.files { elems.push( Node { node: Some(node::Node::File(e)), } .try_into_name_and_node()?, ) } for e in value.symlinks { elems.push( Node { node: Some(node::Node::Symlink(e)), } .try_into_name_and_node()?, ) } crate::Directory::try_from_iter(elems) } } impl From<crate::Directory> for Directory { fn from(value: crate::Directory) -> Self { let mut directories = vec![]; let mut files = vec![]; let mut symlinks = vec![]; for (name, node) in value.into_nodes() { match node { crate::Node::File { digest, size, executable, } => files.push(FileNode { name: name.into(), digest: digest.into(), size, executable, }), crate::Node::Directory { digest, size } => directories.push(DirectoryNode { name: name.into(), digest: digest.into(), size, }), crate::Node::Symlink { target } => { symlinks.push(SymlinkNode { name: name.into(), target: target.into(), }); } } } Directory { directories, files, symlinks, } } } impl Node { /// Converts a proto [Node] to a [crate::Node], and splits off the name as a [PathComponent]. pub fn try_into_name_and_node(self) -> Result<(PathComponent, crate::Node), DirectoryError> { let (name_bytes, node) = self.try_into_unchecked_name_and_checked_node()?; Ok(( name_bytes.try_into().map_err(DirectoryError::InvalidName)?, node, )) } /// Converts a proto [Node] to a [crate::Node], and splits off the name as a /// [bytes::Bytes] without doing any checking of it. fn try_into_unchecked_name_and_checked_node( self, ) -> Result<(bytes::Bytes, crate::Node), DirectoryError> { match self.node.ok_or_else(|| DirectoryError::NoNodeSet)? { node::Node::Directory(n) => { let digest = B3Digest::try_from(n.digest) .map_err(|e| DirectoryError::InvalidNode(n.name.clone(), e.into()))?; let node = crate::Node::Directory { digest, size: n.size, }; Ok((n.name, node)) } node::Node::File(n) => { let digest = B3Digest::try_from(n.digest) .map_err(|e| DirectoryError::InvalidNode(n.name.clone(), e.into()))?; let node = crate::Node::File { digest, size: n.size, executable: n.executable, }; Ok((n.name, node)) } node::Node::Symlink(n) => { let node = crate::Node::Symlink { target: n.target.try_into().map_err(|e| { DirectoryError::InvalidNode( n.name.clone(), crate::ValidateNodeError::InvalidSymlinkTarget(e), ) })?, }; Ok((n.name, node)) } } } /// Converts a proto [Node] to a [crate::Node], and splits off the name and returns it as a /// [bytes::Bytes]. /// /// The name must be empty. pub fn try_into_anonymous_node(self) -> Result<crate::Node, DirectoryError> { let (name, node) = Self::try_into_unchecked_name_and_checked_node(self)?; if !name.is_empty() { return Err(DirectoryError::NameInAnonymousNode); } Ok(node) } /// Constructs a [Node] from a name and [crate::Node]. /// The name is a [bytes::Bytes], not a [PathComponent], as we have use an /// empty name in some places. pub fn from_name_and_node(name: bytes::Bytes, n: crate::Node) -> Self { match n { crate::Node::Directory { digest, size } => Self { node: Some(node::Node::Directory(DirectoryNode { name, digest: digest.into(), size, })), }, crate::Node::File { digest, size, executable, } => Self { node: Some(node::Node::File(FileNode { name, digest: digest.into(), size, executable, })), }, crate::Node::Symlink { target } => Self { node: Some(node::Node::Symlink(SymlinkNode { name, target: target.into(), })), }, } } } impl StatBlobResponse { /// Validates a StatBlobResponse. All chunks must have valid blake3 digests. /// It is allowed to send an empty list, if no more granular chunking is /// available. pub fn validate(&self) -> Result<(), ValidateStatBlobResponseError> { for (i, chunk) in self.chunks.iter().enumerate() { if chunk.digest.len() != blake3::KEY_LEN { return Err(ValidateStatBlobResponseError::InvalidDigestLen( chunk.digest.len(), i, )); } } Ok(()) } }