diff options
Diffstat (limited to 'tvix/castore/src/path/component.rs')
-rw-r--r-- | tvix/castore/src/path/component.rs | 218 |
1 files changed, 192 insertions, 26 deletions
diff --git a/tvix/castore/src/path/component.rs b/tvix/castore/src/path/component.rs index f755f06e62a8..78aca03c50fe 100644 --- a/tvix/castore/src/path/component.rs +++ b/tvix/castore/src/path/component.rs @@ -1,6 +1,3 @@ -// TODO: split out this error -use crate::DirectoryError; - use bstr::ByteSlice; use std::fmt::{self, Debug, Display}; @@ -8,12 +5,17 @@ use std::fmt::{self, Debug, Display}; /// 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() @@ -26,18 +28,24 @@ impl From<PathComponent> for bytes::Bytes { } } -pub(super) fn is_valid_name<B: AsRef<[u8]>>(name: B) -> bool { - let v = name.as_ref(); - - !v.is_empty() && v != *b".." && v != *b"." && !v.contains(&0x00) && !v.contains(&b'/') +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 = DirectoryError; + type Error = PathComponentError; fn try_from(value: bytes::Bytes) -> Result<Self, Self::Error> { - if !is_valid_name(&value) { - return Err(DirectoryError::InvalidName(value)); + if let Err(e) = validate_name(&value) { + return Err(PathComponentError::Convert(value, Box::new(e))); } Ok(Self { inner: value }) @@ -45,14 +53,13 @@ impl TryFrom<bytes::Bytes> for PathComponent { } impl TryFrom<&'static [u8]> for PathComponent { - type Error = DirectoryError; + type Error = PathComponentError; fn try_from(value: &'static [u8]) -> Result<Self, Self::Error> { - if !is_valid_name(value) { - return Err(DirectoryError::InvalidName(bytes::Bytes::from_static( - value, - ))); + if let Err(e) = validate_name(value) { + return Err(PathComponentError::Convert(value.into(), Box::new(e))); } + Ok(Self { inner: bytes::Bytes::from_static(value), }) @@ -60,14 +67,16 @@ impl TryFrom<&'static [u8]> for PathComponent { } impl TryFrom<&str> for PathComponent { - type Error = DirectoryError; + type Error = PathComponentError; fn try_from(value: &str) -> Result<Self, Self::Error> { - if !is_valid_name(value) { - return Err(DirectoryError::InvalidName(bytes::Bytes::copy_from_slice( - value.as_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.as_bytes()), }) @@ -75,16 +84,19 @@ impl TryFrom<&str> for PathComponent { } impl TryFrom<&std::ffi::CStr> for PathComponent { - type Error = DirectoryError; + type Error = PathComponentError; fn try_from(value: &std::ffi::CStr) -> Result<Self, Self::Error> { - if !is_valid_name(value.to_bytes()) { - return Err(DirectoryError::InvalidName(bytes::Bytes::copy_from_slice( - value.to_bytes(), - ))); + 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.to_bytes()), + inner: bytes::Bytes::copy_from_slice(value), }) } } @@ -100,3 +112,157 @@ impl Display for PathComponent { 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" + ); + } + } +} |