about summary refs log tree commit diff
path: root/tvix/castore/src/path/component.rs
use bstr::ByteSlice;
use std::fmt::{self, Debug, Display};

/// A wrapper type for validated path components in the castore model.
/// Internally uses a [bytes::Bytes], but disallows
/// slashes, and null bytes to be present, as well as
/// '.', '..' and the empty string.
/// It also rejects components that are too long (> 255 bytes).
#[repr(transparent)]
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct PathComponent {
    pub(super) inner: bytes::Bytes,
}

/// The maximum length an individual path component can have.
/// Linux allows 255 bytes of actual name, so we pick that.
pub const MAX_NAME_LEN: usize = 255;

impl AsRef<[u8]> for PathComponent {
    fn as_ref(&self) -> &[u8] {
        self.inner.as_ref()
    }
}

impl From<PathComponent> for bytes::Bytes {
    fn from(value: PathComponent) -> Self {
        value.inner
    }
}

pub(super) fn validate_name<B: AsRef<[u8]>>(name: B) -> Result<(), PathComponentError> {
    match name.as_ref() {
        b"" => Err(PathComponentError::Empty),
        b".." => Err(PathComponentError::Parent),
        b"." => Err(PathComponentError::CurDir),
        v if v.len() > MAX_NAME_LEN => Err(PathComponentError::TooLong),
        v if v.contains(&0x00) => Err(PathComponentError::Null),
        v if v.contains(&b'/') => Err(PathComponentError::Slashes),
        _ => Ok(()),
    }
}

impl TryFrom<bytes::Bytes> for PathComponent {
    type Error = PathComponentError;

    fn try_from(value: bytes::Bytes) -> Result<Self, Self::Error> {
        if let Err(e) = validate_name(&value) {
            return Err(PathComponentError::Convert(value, Box::new(e)));
        }

        Ok(Self { inner: value })
    }
}

impl TryFrom<&'static [u8]> for PathComponent {
    type Error = PathComponentError;

    fn try_from(value: &'static [u8]) -> Result<Self, Self::Error> {
        if let Err(e) = validate_name(value) {
            return Err(PathComponentError::Convert(value.into(), Box::new(e)));
        }

        Ok(Self {
            inner: bytes::Bytes::from_static(value),
        })
    }
}

impl TryFrom<&str> for PathComponent {
    type Error = PathComponentError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        if let Err(e) = validate_name(value) {
            return Err(PathComponentError::Convert(
                value.to_owned().into(),
                Box::new(e),
            ));
        }

        Ok(Self {
            inner: bytes::Bytes::copy_from_slice(value.as_bytes()),
        })
    }
}

impl TryFrom<&std::ffi::CStr> for PathComponent {
    type Error = PathComponentError;

    fn try_from(value: &std::ffi::CStr) -> Result<Self, Self::Error> {
        let value = value.to_bytes();
        if let Err(e) = validate_name(value) {
            return Err(PathComponentError::Convert(
                value.to_owned().into(),
                Box::new(e),
            ));
        }

        Ok(Self {
            inner: bytes::Bytes::copy_from_slice(value),
        })
    }
}

impl Debug for PathComponent {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        Debug::fmt(self.inner.as_bstr(), f)
    }
}

impl Display for PathComponent {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        Display::fmt(self.inner.as_bstr(), f)
    }
}

/// Errors created when parsing / validating [PathComponent].
#[derive(Debug, PartialEq, thiserror::Error)]
#[cfg_attr(test, derive(Clone))]
pub enum PathComponentError {
    #[error("cannot be empty")]
    Empty,
    #[error("cannot contain null bytes")]
    Null,
    #[error("cannot be '.'")]
    CurDir,
    #[error("cannot be '..'")]
    Parent,
    #[error("cannot contain slashes")]
    Slashes,
    #[error("cannot be over {} bytes long", MAX_NAME_LEN)]
    TooLong,
    #[error("unable to convert '{:?}'", .0.as_bstr())]
    Convert(bytes::Bytes, #[source] Box<Self>),
}

#[cfg(test)]
mod tests {
    use std::ffi::CString;

    use bytes::Bytes;
    use rstest::rstest;

    use super::{validate_name, PathComponent, PathComponentError};

    #[rstest]
    #[case::empty(b"", PathComponentError::Empty)]
    #[case::null(b"foo\0", PathComponentError::Null)]
    #[case::curdir(b".", PathComponentError::CurDir)]
    #[case::parent(b"..", PathComponentError::Parent)]
    #[case::slashes1(b"a/b", PathComponentError::Slashes)]
    #[case::slashes2(b"/", PathComponentError::Slashes)]
    fn errors(#[case] v: &'static [u8], #[case] err: PathComponentError) {
        {
            assert_eq!(
                Err(err.clone()),
                validate_name(v),
                "validate_name must fail as expected"
            );
        }

        let exp_err_v = Bytes::from_static(v);

        // Bytes
        {
            let v = Bytes::from_static(v);
            assert_eq!(
                Err(PathComponentError::Convert(
                    exp_err_v.clone(),
                    Box::new(err.clone())
                )),
                PathComponent::try_from(v),
                "conversion must fail as expected"
            );
        }
        // &[u8]
        {
            assert_eq!(
                Err(PathComponentError::Convert(
                    exp_err_v.clone(),
                    Box::new(err.clone())
                )),
                PathComponent::try_from(v),
                "conversion must fail as expected"
            );
        }
        // &str, if it is valid UTF-8
        {
            if let Ok(v) = std::str::from_utf8(v) {
                assert_eq!(
                    Err(PathComponentError::Convert(
                        exp_err_v.clone(),
                        Box::new(err.clone())
                    )),
                    PathComponent::try_from(v),
                    "conversion must fail as expected"
                );
            }
        }
        // &CStr, if it can be constructed (fails if the payload contains null bytes)
        {
            if let Ok(v) = CString::new(v) {
                let v = v.as_ref();
                assert_eq!(
                    Err(PathComponentError::Convert(
                        exp_err_v.clone(),
                        Box::new(err.clone())
                    )),
                    PathComponent::try_from(v),
                    "conversion must fail as expected"
                );
            }
        }
    }

    #[test]
    fn error_toolong() {
        assert_eq!(
            Err(PathComponentError::TooLong),
            validate_name("X".repeat(500).into_bytes().as_slice())
        )
    }

    #[test]
    fn success() {
        let exp = PathComponent { inner: "aa".into() };

        // Bytes
        {
            let v: Bytes = "aa".into();
            assert_eq!(
                Ok(exp.clone()),
                PathComponent::try_from(v),
                "conversion must succeed"
            );
        }

        // &[u8]
        {
            let v: &[u8] = b"aa";
            assert_eq!(
                Ok(exp.clone()),
                PathComponent::try_from(v),
                "conversion must succeed"
            );
        }

        // &str
        {
            let v: &str = "aa";
            assert_eq!(
                Ok(exp.clone()),
                PathComponent::try_from(v),
                "conversion must succeed"
            );
        }

        // &CStr
        {
            let v = CString::new("aa").expect("CString must construct");
            let v = v.as_c_str();
            assert_eq!(
                Ok(exp.clone()),
                PathComponent::try_from(v),
                "conversion must succeed"
            );
        }
    }
}