snix_castore/path/
component.rs1use bstr::ByteSlice;
2use std::fmt::{self, Debug, Display};
3
4#[repr(transparent)]
10#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
11pub struct PathComponent {
12 pub(super) inner: bytes::Bytes,
13}
14
15impl PathComponent {
16 pub fn extension(&self) -> Option<&[u8]> {
18 let mut iter = self.inner[..].rsplitn(2, |b| *b == b'.');
19 let e = iter.next();
20 iter.next()?;
22
23 e
24 }
25}
26
27pub 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#[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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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}