snix_castore/nodes/
symlink_target.rs1use bstr::ByteSlice;
2use std::fmt::{self, Debug, Display};
3
4#[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
14pub 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#[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 {
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 {
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 {
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 {
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 {
206 assert_eq!(
207 Ok(exp.clone()),
208 SymlinkTarget::try_from(v),
209 "conversion must succeed"
210 )
211 }
212
213 {
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}