snix_castore/nodes/
symlink_target.rs1use bstr::ByteSlice;
2use std::fmt::{self, Debug, Display};
3
4#[repr(transparent)]
8#[derive(Clone, PartialEq, Eq)]
9pub struct SymlinkTarget {
10 inner: bytes::Bytes,
11}
12
13pub 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#[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 {
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 {
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 {
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 {
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 {
205 assert_eq!(
206 Ok(exp.clone()),
207 SymlinkTarget::try_from(v),
208 "conversion must succeed"
209 )
210 }
211
212 {
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}