snix_castore/nodes/
symlink_target.rs

1use bstr::ByteSlice;
2use std::fmt::{self, Debug, Display};
3
4/// A wrapper type for symlink targets.
5/// Internally uses a [bytes::Bytes], but disallows empty targets and those
6/// containing null bytes.
7#[repr(transparent)]
8#[derive(Clone, PartialEq, Eq)]
9pub struct SymlinkTarget {
10    inner: bytes::Bytes,
11}
12
13/// The maximum length a symlink target can have.
14/// Linux allows 4095 bytes here.
15pub const MAX_TARGET_LEN: usize = 4095;
16
17impl AsRef<[u8]> for SymlinkTarget {
18    fn as_ref(&self) -> &[u8] {
19        self.inner.as_ref()
20    }
21}
22
23impl From<SymlinkTarget> for bytes::Bytes {
24    fn from(value: SymlinkTarget) -> Self {
25        value.inner
26    }
27}
28
29fn validate_symlink_target<B: AsRef<[u8]>>(symlink_target: B) -> Result<B, SymlinkTargetError> {
30    let v = symlink_target.as_ref();
31
32    if v.is_empty() {
33        return Err(SymlinkTargetError::Empty);
34    }
35    if v.len() > MAX_TARGET_LEN {
36        return Err(SymlinkTargetError::TooLong);
37    }
38    if v.contains(&0x00) {
39        return Err(SymlinkTargetError::Null);
40    }
41
42    Ok(symlink_target)
43}
44
45impl TryFrom<bytes::Bytes> for SymlinkTarget {
46    type Error = SymlinkTargetError;
47
48    fn try_from(value: bytes::Bytes) -> Result<Self, Self::Error> {
49        if let Err(e) = validate_symlink_target(&value) {
50            return Err(SymlinkTargetError::Convert(value, Box::new(e)));
51        }
52
53        Ok(Self { inner: value })
54    }
55}
56
57impl TryFrom<&'static [u8]> for SymlinkTarget {
58    type Error = SymlinkTargetError;
59
60    fn try_from(value: &'static [u8]) -> Result<Self, Self::Error> {
61        if let Err(e) = validate_symlink_target(&value) {
62            return Err(SymlinkTargetError::Convert(value.into(), Box::new(e)));
63        }
64
65        Ok(Self {
66            inner: bytes::Bytes::from_static(value),
67        })
68    }
69}
70
71impl TryFrom<&str> for SymlinkTarget {
72    type Error = SymlinkTargetError;
73
74    fn try_from(value: &str) -> Result<Self, Self::Error> {
75        if let Err(e) = validate_symlink_target(value) {
76            return Err(SymlinkTargetError::Convert(
77                value.to_owned().into(),
78                Box::new(e),
79            ));
80        }
81
82        Ok(Self {
83            inner: bytes::Bytes::copy_from_slice(value.as_bytes()),
84        })
85    }
86}
87
88impl Debug for SymlinkTarget {
89    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
90        Debug::fmt(self.inner.as_bstr(), f)
91    }
92}
93
94impl Display for SymlinkTarget {
95    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
96        Display::fmt(self.inner.as_bstr(), f)
97    }
98}
99
100/// Errors created when constructing / converting to [SymlinkTarget].
101#[derive(Debug, PartialEq, Eq, thiserror::Error)]
102#[cfg_attr(test, derive(Clone))]
103pub enum SymlinkTargetError {
104    #[error("cannot be empty")]
105    Empty,
106    #[error("cannot contain null bytes")]
107    Null,
108    #[error("cannot be over {} bytes long", MAX_TARGET_LEN)]
109    TooLong,
110    #[error("unable to convert '{:?}", .0.as_bstr())]
111    Convert(bytes::Bytes, Box<Self>),
112}
113
114#[cfg(test)]
115mod tests {
116    use bytes::Bytes;
117    use rstest::rstest;
118
119    use super::validate_symlink_target;
120    use super::{SymlinkTarget, SymlinkTargetError};
121
122    #[rstest]
123    #[case::empty(b"", SymlinkTargetError::Empty)]
124    #[case::null(b"foo\0", SymlinkTargetError::Null)]
125    fn errors(#[case] v: &'static [u8], #[case] err: SymlinkTargetError) {
126        {
127            assert_eq!(
128                Err(err.clone()),
129                validate_symlink_target(v),
130                "validate_symlink_target must fail as expected"
131            );
132        }
133
134        let exp_err_v = Bytes::from_static(v);
135
136        // Bytes
137        {
138            let v = Bytes::from_static(v);
139            assert_eq!(
140                Err(SymlinkTargetError::Convert(
141                    exp_err_v.clone(),
142                    Box::new(err.clone())
143                )),
144                SymlinkTarget::try_from(v),
145                "conversion must fail as expected"
146            );
147        }
148        // &[u8]
149        {
150            assert_eq!(
151                Err(SymlinkTargetError::Convert(
152                    exp_err_v.clone(),
153                    Box::new(err.clone())
154                )),
155                SymlinkTarget::try_from(v),
156                "conversion must fail as expected"
157            );
158        }
159        // &str, if this is valid UTF-8
160        {
161            if let Ok(v) = std::str::from_utf8(v) {
162                assert_eq!(
163                    Err(SymlinkTargetError::Convert(
164                        exp_err_v.clone(),
165                        Box::new(err.clone())
166                    )),
167                    SymlinkTarget::try_from(v),
168                    "conversion must fail as expected"
169                );
170            }
171        }
172    }
173
174    #[test]
175    fn error_toolong() {
176        assert_eq!(
177            Err(SymlinkTargetError::TooLong),
178            validate_symlink_target("X".repeat(5000).into_bytes().as_slice())
179        )
180    }
181
182    #[rstest]
183    #[case::boring(b"aa")]
184    #[case::dot(b".")]
185    #[case::dotsandslashes(b"./..")]
186    #[case::dotdot(b"..")]
187    #[case::slashes(b"a/b")]
188    #[case::slashes_and_absolute(b"/a/b")]
189    #[case::invalid_utf8(b"\xc5\xc4\xd6")]
190    fn success(#[case] v: &'static [u8]) {
191        let exp = SymlinkTarget { inner: v.into() };
192
193        // Bytes
194        {
195            let v: Bytes = v.into();
196            assert_eq!(
197                Ok(exp.clone()),
198                SymlinkTarget::try_from(v),
199                "conversion must succeed"
200            )
201        }
202
203        // &[u8]
204        {
205            assert_eq!(
206                Ok(exp.clone()),
207                SymlinkTarget::try_from(v),
208                "conversion must succeed"
209            )
210        }
211
212        // &str, if this is valid UTF-8
213        {
214            if let Ok(v) = std::str::from_utf8(v) {
215                assert_eq!(
216                    Ok(exp.clone()),
217                    SymlinkTarget::try_from(v),
218                    "conversion must succeed"
219                )
220            }
221        }
222    }
223}