snix_castore/path/
component.rs

1use bstr::ByteSlice;
2use std::fmt::{self, Debug, Display};
3
4/// A wrapper type for validated path components in the castore model.
5/// Internally uses a [bytes::Bytes], but disallows
6/// slashes, and null bytes to be present, as well as
7/// '.', '..' and the empty string.
8/// It also rejects components that are too long (> 255 bytes).
9#[repr(transparent)]
10#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
11pub struct PathComponent {
12    pub(super) inner: bytes::Bytes,
13}
14
15impl PathComponent {
16    /// Extracts the extension (without leading dot) of a [PathComponent], if possible.
17    pub fn extension(&self) -> Option<&[u8]> {
18        let mut iter = self.inner[..].rsplitn(2, |b| *b == b'.');
19        let e = iter.next();
20        // Return None if there's no dot.
21        iter.next()?;
22
23        e
24    }
25}
26
27/// The maximum length an individual path component can have.
28/// Linux allows 255 bytes of actual name, so we pick that.
29pub const MAX_NAME_LEN: usize = 255;
30
31impl AsRef<[u8]> for PathComponent {
32    fn as_ref(&self) -> &[u8] {
33        self.inner.as_ref()
34    }
35}
36
37impl From<PathComponent> for bytes::Bytes {
38    fn from(value: PathComponent) -> Self {
39        value.inner
40    }
41}
42
43pub(super) fn validate_name<B: AsRef<[u8]>>(name: B) -> Result<(), PathComponentError> {
44    match name.as_ref() {
45        b"" => Err(PathComponentError::Empty),
46        b".." => Err(PathComponentError::Parent),
47        b"." => Err(PathComponentError::CurDir),
48        v if v.len() > MAX_NAME_LEN => Err(PathComponentError::TooLong),
49        v if v.contains(&0x00) => Err(PathComponentError::Null),
50        v if v.contains(&b'/') => Err(PathComponentError::Slashes),
51        _ => Ok(()),
52    }
53}
54
55impl TryFrom<bytes::Bytes> for PathComponent {
56    type Error = PathComponentError;
57
58    fn try_from(value: bytes::Bytes) -> Result<Self, Self::Error> {
59        if let Err(e) = validate_name(&value) {
60            return Err(PathComponentError::Convert(value, Box::new(e)));
61        }
62
63        Ok(Self { inner: value })
64    }
65}
66
67impl TryFrom<&'static [u8]> for PathComponent {
68    type Error = PathComponentError;
69
70    fn try_from(value: &'static [u8]) -> Result<Self, Self::Error> {
71        if let Err(e) = validate_name(value) {
72            return Err(PathComponentError::Convert(value.into(), Box::new(e)));
73        }
74
75        Ok(Self {
76            inner: bytes::Bytes::from_static(value),
77        })
78    }
79}
80
81impl TryFrom<&str> for PathComponent {
82    type Error = PathComponentError;
83
84    fn try_from(value: &str) -> Result<Self, Self::Error> {
85        if let Err(e) = validate_name(value) {
86            return Err(PathComponentError::Convert(
87                value.to_owned().into(),
88                Box::new(e),
89            ));
90        }
91
92        Ok(Self {
93            inner: bytes::Bytes::copy_from_slice(value.as_bytes()),
94        })
95    }
96}
97
98impl TryFrom<&std::ffi::CStr> for PathComponent {
99    type Error = PathComponentError;
100
101    fn try_from(value: &std::ffi::CStr) -> Result<Self, Self::Error> {
102        let value = value.to_bytes();
103        if let Err(e) = validate_name(value) {
104            return Err(PathComponentError::Convert(
105                value.to_owned().into(),
106                Box::new(e),
107            ));
108        }
109
110        Ok(Self {
111            inner: bytes::Bytes::copy_from_slice(value),
112        })
113    }
114}
115
116impl Debug for PathComponent {
117    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
118        Debug::fmt(self.inner.as_bstr(), f)
119    }
120}
121
122impl Display for PathComponent {
123    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
124        Display::fmt(self.inner.as_bstr(), f)
125    }
126}
127
128/// Errors created when parsing / validating [PathComponent].
129#[derive(Debug, PartialEq, thiserror::Error)]
130#[cfg_attr(test, derive(Clone))]
131pub enum PathComponentError {
132    #[error("cannot be empty")]
133    Empty,
134    #[error("cannot contain null bytes")]
135    Null,
136    #[error("cannot be '.'")]
137    CurDir,
138    #[error("cannot be '..'")]
139    Parent,
140    #[error("cannot contain slashes")]
141    Slashes,
142    #[error("cannot be over {} bytes long", MAX_NAME_LEN)]
143    TooLong,
144    #[error("unable to convert '{:?}'", .0.as_bstr())]
145    Convert(bytes::Bytes, #[source] Box<Self>),
146}
147
148#[cfg(test)]
149mod tests {
150    use std::ffi::CString;
151
152    use bytes::Bytes;
153    use rstest::rstest;
154
155    use super::{PathComponent, PathComponentError, validate_name};
156
157    #[rstest]
158    #[case::empty(b"", PathComponentError::Empty)]
159    #[case::null(b"foo\0", PathComponentError::Null)]
160    #[case::curdir(b".", PathComponentError::CurDir)]
161    #[case::parent(b"..", PathComponentError::Parent)]
162    #[case::slashes1(b"a/b", PathComponentError::Slashes)]
163    #[case::slashes2(b"/", PathComponentError::Slashes)]
164    fn errors(#[case] v: &'static [u8], #[case] err: PathComponentError) {
165        {
166            assert_eq!(
167                Err(err.clone()),
168                validate_name(v),
169                "validate_name must fail as expected"
170            );
171        }
172
173        let exp_err_v = Bytes::from_static(v);
174
175        // Bytes
176        {
177            let v = Bytes::from_static(v);
178            assert_eq!(
179                Err(PathComponentError::Convert(
180                    exp_err_v.clone(),
181                    Box::new(err.clone())
182                )),
183                PathComponent::try_from(v),
184                "conversion must fail as expected"
185            );
186        }
187        // &[u8]
188        {
189            assert_eq!(
190                Err(PathComponentError::Convert(
191                    exp_err_v.clone(),
192                    Box::new(err.clone())
193                )),
194                PathComponent::try_from(v),
195                "conversion must fail as expected"
196            );
197        }
198        // &str, if it is valid UTF-8
199        {
200            if let Ok(v) = std::str::from_utf8(v) {
201                assert_eq!(
202                    Err(PathComponentError::Convert(
203                        exp_err_v.clone(),
204                        Box::new(err.clone())
205                    )),
206                    PathComponent::try_from(v),
207                    "conversion must fail as expected"
208                );
209            }
210        }
211        // &CStr, if it can be constructed (fails if the payload contains null bytes)
212        {
213            if let Ok(v) = CString::new(v) {
214                let v = v.as_ref();
215                assert_eq!(
216                    Err(PathComponentError::Convert(
217                        exp_err_v.clone(),
218                        Box::new(err.clone())
219                    )),
220                    PathComponent::try_from(v),
221                    "conversion must fail as expected"
222                );
223            }
224        }
225    }
226
227    #[rstest]
228    #[case::without_dot(PathComponent { inner: "foo".into()}, None)]
229    #[case::simple(PathComponent { inner: "foo.txt".into()}, Some(&b"txt"[..]))]
230    #[case::empty(PathComponent { inner: "foo.".into()}, Some(&b""[..]))]
231    #[case::multiple(PathComponent { inner: "foo.bar.txt".into()}, Some(&b"txt"[..]))]
232    fn extension(#[case] p: PathComponent, #[case] exp_extension: Option<&[u8]>) {
233        assert_eq!(exp_extension, p.extension())
234    }
235
236    #[test]
237    fn error_toolong() {
238        assert_eq!(
239            Err(PathComponentError::TooLong),
240            validate_name("X".repeat(500).into_bytes().as_slice())
241        )
242    }
243
244    #[test]
245    fn success() {
246        let exp = PathComponent { inner: "aa".into() };
247
248        // Bytes
249        {
250            let v: Bytes = "aa".into();
251            assert_eq!(
252                Ok(exp.clone()),
253                PathComponent::try_from(v),
254                "conversion must succeed"
255            );
256        }
257
258        // &[u8]
259        {
260            let v: &[u8] = b"aa";
261            assert_eq!(
262                Ok(exp.clone()),
263                PathComponent::try_from(v),
264                "conversion must succeed"
265            );
266        }
267
268        // &str
269        {
270            let v: &str = "aa";
271            assert_eq!(
272                Ok(exp.clone()),
273                PathComponent::try_from(v),
274                "conversion must succeed"
275            );
276        }
277
278        // &CStr
279        {
280            let v = CString::new("aa").expect("CString must construct");
281            let v = v.as_c_str();
282            assert_eq!(
283                Ok(exp.clone()),
284                PathComponent::try_from(v),
285                "conversion must succeed"
286            );
287        }
288    }
289}