refactor(tvix/castore): have SymlinkTarget-specific errors

Don't use ValidateNodeError, but SymlinkTargetError.

Also, add checks for too long symlink targets.

Change-Id: I4b533325d494232ff9d0b3f4f695f5a1a0a36199
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12230
Autosubmit: flokli <flokli@flokli.de>
Reviewed-by: edef <edef@edef.eu>
Reviewed-by: Ilan Joselevich <personal@ilanjoselevich.com>
Tested-by: BuildkiteCI
This commit is contained in:
Florian Klink 2024-08-17 22:00:50 +03:00 committed by clbot
parent 56fa533e43
commit e086c76ee9
5 changed files with 172 additions and 26 deletions

View file

@ -3,7 +3,10 @@ use thiserror::Error;
use tokio::task::JoinError;
use tonic::Status;
use crate::path::{PathComponent, PathComponentError};
use crate::{
path::{PathComponent, PathComponentError},
SymlinkTargetError,
};
/// Errors related to communication with the store.
#[derive(Debug, Error, PartialEq)]
@ -22,8 +25,8 @@ pub enum ValidateNodeError {
#[error("invalid digest length: {0}")]
InvalidDigestLen(usize),
/// Invalid symlink target
#[error("Invalid symlink target: {}", .0.as_bstr())]
InvalidSymlinkTarget(bytes::Bytes),
#[error("Invalid symlink target: {0}")]
InvalidSymlinkTarget(SymlinkTargetError),
}
impl From<crate::digests::Error> for ValidateNodeError {

View file

@ -6,7 +6,7 @@
use crate::directoryservice::{DirectoryPutter, DirectoryService};
use crate::path::{Path, PathBuf};
use crate::{B3Digest, Directory, Node};
use crate::{B3Digest, Directory, Node, SymlinkTargetError};
use futures::{Stream, StreamExt};
use tracing::Level;
@ -91,10 +91,10 @@ where
}
IngestionEntry::Symlink { ref target, .. } => Node::Symlink {
target: bytes::Bytes::copy_from_slice(target).try_into().map_err(
|e: crate::ValidateNodeError| {
|e: SymlinkTargetError| {
IngestionError::UploadDirectoryError(
entry.path().to_owned(),
crate::Error::StorageError(e.to_string()),
crate::Error::StorageError(format!("invalid symlink target: {}", e)),
)
},
)?,

View file

@ -4,7 +4,7 @@ mod symlink_target;
use crate::B3Digest;
pub use directory::Directory;
pub use symlink_target::SymlinkTarget;
pub use symlink_target::{SymlinkTarget, SymlinkTargetError};
/// A Node is either a [DirectoryNode], [FileNode] or [SymlinkNode].
/// Nodes themselves don't have names, what gives them names is either them

View file

@ -1,6 +1,3 @@
// TODO: split out this error
use crate::ValidateNodeError;
use bstr::ByteSlice;
use std::fmt::{self, Debug, Display};
@ -13,6 +10,10 @@ pub struct SymlinkTarget {
inner: bytes::Bytes,
}
/// The maximum length a symlink target can have.
/// Linux allows 4095 bytes here.
pub const MAX_TARGET_LEN: usize = 4095;
impl AsRef<[u8]> for SymlinkTarget {
fn as_ref(&self) -> &[u8] {
self.inner.as_ref()
@ -25,12 +26,28 @@ impl From<SymlinkTarget> for bytes::Bytes {
}
}
fn validate_symlink_target<B: AsRef<[u8]>>(symlink_target: B) -> Result<B, SymlinkTargetError> {
let v = symlink_target.as_ref();
if v.is_empty() {
return Err(SymlinkTargetError::Empty);
}
if v.len() > MAX_TARGET_LEN {
return Err(SymlinkTargetError::TooLong);
}
if v.contains(&0x00) {
return Err(SymlinkTargetError::Null);
}
Ok(symlink_target)
}
impl TryFrom<bytes::Bytes> for SymlinkTarget {
type Error = ValidateNodeError;
type Error = SymlinkTargetError;
fn try_from(value: bytes::Bytes) -> Result<Self, Self::Error> {
if value.is_empty() || value.contains(&b'\0') {
return Err(ValidateNodeError::InvalidSymlinkTarget(value));
if let Err(e) = validate_symlink_target(&value) {
return Err(SymlinkTargetError::Convert(value, Box::new(e)));
}
Ok(Self { inner: value })
@ -38,13 +55,11 @@ impl TryFrom<bytes::Bytes> for SymlinkTarget {
}
impl TryFrom<&'static [u8]> for SymlinkTarget {
type Error = ValidateNodeError;
type Error = SymlinkTargetError;
fn try_from(value: &'static [u8]) -> Result<Self, Self::Error> {
if value.is_empty() || value.contains(&b'\0') {
return Err(ValidateNodeError::InvalidSymlinkTarget(
bytes::Bytes::from_static(value),
));
if let Err(e) = validate_symlink_target(&value) {
return Err(SymlinkTargetError::Convert(value.into(), Box::new(e)));
}
Ok(Self {
@ -54,12 +69,13 @@ impl TryFrom<&'static [u8]> for SymlinkTarget {
}
impl TryFrom<&str> for SymlinkTarget {
type Error = ValidateNodeError;
type Error = SymlinkTargetError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.is_empty() {
return Err(ValidateNodeError::InvalidSymlinkTarget(
bytes::Bytes::copy_from_slice(value.as_bytes()),
if let Err(e) = validate_symlink_target(value) {
return Err(SymlinkTargetError::Convert(
value.to_owned().into(),
Box::new(e),
));
}
@ -80,3 +96,128 @@ impl Display for SymlinkTarget {
Display::fmt(self.inner.as_bstr(), f)
}
}
/// Errors created when constructing / converting to [SymlinkTarget].
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(test, derive(Clone))]
pub enum SymlinkTargetError {
#[error("cannot be empty")]
Empty,
#[error("cannot contain null bytes")]
Null,
#[error("cannot be over {} bytes long", MAX_TARGET_LEN)]
TooLong,
#[error("unable to convert '{:?}", .0.as_bstr())]
Convert(bytes::Bytes, Box<Self>),
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use rstest::rstest;
use super::validate_symlink_target;
use super::{SymlinkTarget, SymlinkTargetError};
#[rstest]
#[case::empty(b"", SymlinkTargetError::Empty)]
#[case::null(b"foo\0", SymlinkTargetError::Null)]
fn errors(#[case] v: &'static [u8], #[case] err: SymlinkTargetError) {
{
assert_eq!(
Err(err.clone()),
validate_symlink_target(v),
"validate_symlink_target must fail as expected"
);
}
let exp_err_v = Bytes::from_static(v);
// Bytes
{
let v = Bytes::from_static(v);
assert_eq!(
Err(SymlinkTargetError::Convert(
exp_err_v.clone(),
Box::new(err.clone())
)),
SymlinkTarget::try_from(v),
"conversion must fail as expected"
);
}
// &[u8]
{
assert_eq!(
Err(SymlinkTargetError::Convert(
exp_err_v.clone(),
Box::new(err.clone())
)),
SymlinkTarget::try_from(v),
"conversion must fail as expected"
);
}
// &str, if this is valid UTF-8
{
if let Ok(v) = std::str::from_utf8(v) {
assert_eq!(
Err(SymlinkTargetError::Convert(
exp_err_v.clone(),
Box::new(err.clone())
)),
SymlinkTarget::try_from(v),
"conversion must fail as expected"
);
}
}
}
#[test]
fn error_toolong() {
assert_eq!(
Err(SymlinkTargetError::TooLong),
validate_symlink_target("X".repeat(5000).into_bytes().as_slice())
)
}
#[rstest]
#[case::boring(b"aa")]
#[case::dot(b".")]
#[case::dotsandslashes(b"./..")]
#[case::dotdot(b"..")]
#[case::slashes(b"a/b")]
#[case::slashes_and_absolute(b"/a/b")]
#[case::invalid_utf8(b"\xc5\xc4\xd6")]
fn success(#[case] v: &'static [u8]) {
let exp = SymlinkTarget { inner: v.into() };
// Bytes
{
let v: Bytes = v.into();
assert_eq!(
Ok(exp.clone()),
SymlinkTarget::try_from(v),
"conversion must succeed"
)
}
// &[u8]
{
assert_eq!(
Ok(exp.clone()),
SymlinkTarget::try_from(v),
"conversion must succeed"
)
}
// &str, if this is valid UTF-8
{
if let Ok(v) = std::str::from_utf8(v) {
assert_eq!(
Ok(exp.clone()),
SymlinkTarget::try_from(v),
"conversion must succeed"
)
}
}
}
}

View file

@ -223,10 +223,12 @@ impl Node {
let name: PathComponent = n.name.try_into().map_err(DirectoryError::InvalidName)?;
let node = crate::Node::Symlink {
target: n
.target
.try_into()
.map_err(|e| DirectoryError::InvalidNode(name.clone(), e))?,
target: n.target.try_into().map_err(|e| {
DirectoryError::InvalidNode(
name.clone(),
crate::ValidateNodeError::InvalidSymlinkTarget(e),
)
})?,
};
Ok((name, node))