Skip to main content

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