typed_path/unix/
non_utf8.rs

1mod components;
2
3use core::fmt;
4use core::hash::Hasher;
5
6pub use components::*;
7
8use super::constants::*;
9use crate::common::CheckedPathError;
10use crate::no_std_compat::*;
11use crate::typed::{TypedPath, TypedPathBuf};
12use crate::{private, Components, Encoding, Path, PathBuf};
13
14/// Represents a Unix-specific [`Path`]
15pub type UnixPath = Path<UnixEncoding>;
16
17/// Represents a Unix-specific [`PathBuf`]
18pub type UnixPathBuf = PathBuf<UnixEncoding>;
19
20/// Represents a Unix-specific [`Encoding`]
21#[derive(Copy, Clone)]
22pub struct UnixEncoding;
23
24impl private::Sealed for UnixEncoding {}
25
26impl Encoding for UnixEncoding {
27    type Components<'a> = UnixComponents<'a>;
28
29    fn label() -> &'static str {
30        "unix"
31    }
32
33    fn components(path: &[u8]) -> Self::Components<'_> {
34        UnixComponents::new(path)
35    }
36
37    fn hash<H: Hasher>(path: &[u8], h: &mut H) {
38        let mut component_start = 0;
39        let mut bytes_hashed = 0;
40
41        for i in 0..path.len() {
42            let is_sep = path[i] == SEPARATOR as u8;
43            if is_sep {
44                if i > component_start {
45                    let to_hash = &path[component_start..i];
46                    h.write(to_hash);
47                    bytes_hashed += to_hash.len();
48                }
49
50                // skip over separator and optionally a following CurDir item
51                // since components() would normalize these away.
52                component_start = i + 1;
53
54                let tail = &path[component_start..];
55
56                component_start += match tail {
57                    [b'.'] => 1,
58                    [b'.', sep, ..] if *sep == SEPARATOR as u8 => 1,
59                    _ => 0,
60                };
61            }
62        }
63
64        if component_start < path.len() {
65            let to_hash = &path[component_start..];
66            h.write(to_hash);
67            bytes_hashed += to_hash.len();
68        }
69
70        h.write_usize(bytes_hashed);
71    }
72
73    fn push(current_path: &mut Vec<u8>, path: &[u8]) {
74        if path.is_empty() {
75            return;
76        }
77
78        // Absolute path will replace entirely, otherwise check if we need to add our separator,
79        // and add it if the separator is missing
80        //
81        // Otherwise, if our current path is not empty, we will append the provided path
82        // to the end with a separator inbetween
83        if Self::components(path).is_absolute() {
84            current_path.clear();
85        } else if !current_path.is_empty() && !current_path.ends_with(&[SEPARATOR as u8]) {
86            current_path.push(SEPARATOR as u8);
87        }
88
89        current_path.extend_from_slice(path);
90    }
91
92    fn push_checked(current_path: &mut Vec<u8>, path: &[u8]) -> Result<(), CheckedPathError> {
93        // As we scan through path components, we maintain a count of normal components that
94        // have not been popped off as a result of a parent component. If we ever reach a
95        // parent component without any preceding normal components remaining, this violates
96        // pushing onto our path and represents a path traversal attack.
97        let mut normal_cnt = 0;
98        for component in UnixPath::new(path).components() {
99            match component {
100                UnixComponent::RootDir => return Err(CheckedPathError::UnexpectedRoot),
101                UnixComponent::ParentDir if normal_cnt == 0 => {
102                    return Err(CheckedPathError::PathTraversalAttack)
103                }
104                UnixComponent::ParentDir => normal_cnt -= 1,
105                UnixComponent::Normal(bytes) => {
106                    for b in bytes {
107                        if DISALLOWED_FILENAME_BYTES.contains(b) {
108                            return Err(CheckedPathError::InvalidFilename);
109                        }
110                    }
111                    normal_cnt += 1;
112                }
113                _ => continue,
114            }
115        }
116
117        Self::push(current_path, path);
118        Ok(())
119    }
120}
121
122impl fmt::Debug for UnixEncoding {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.debug_struct("UnixEncoding").finish()
125    }
126}
127
128impl fmt::Display for UnixEncoding {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "UnixEncoding")
131    }
132}
133
134impl<T> Path<T>
135where
136    T: Encoding,
137{
138    /// Returns true if the encoding for the path is for Unix.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use typed_path::{UnixPath, WindowsPath};
144    ///
145    /// assert!(UnixPath::new("/some/path").has_unix_encoding());
146    /// assert!(!WindowsPath::new(r"\some\path").has_unix_encoding());
147    /// ```
148    pub fn has_unix_encoding(&self) -> bool {
149        T::label() == UnixEncoding::label()
150    }
151
152    /// Creates an owned [`PathBuf`] like `self` but using [`UnixEncoding`].
153    ///
154    /// See [`Path::with_encoding`] for more information.
155    pub fn with_unix_encoding(&self) -> PathBuf<UnixEncoding> {
156        self.with_encoding()
157    }
158
159    /// Creates an owned [`PathBuf`] like `self` but using [`UnixEncoding`], ensuring it is a valid
160    /// Unix path.
161    ///
162    /// See [`Path::with_encoding_checked`] for more information.
163    pub fn with_unix_encoding_checked(&self) -> Result<PathBuf<UnixEncoding>, CheckedPathError> {
164        self.with_encoding_checked()
165    }
166}
167
168impl UnixPath {
169    pub fn to_typed_path(&self) -> TypedPath<'_> {
170        TypedPath::unix(self)
171    }
172
173    pub fn to_typed_path_buf(&self) -> TypedPathBuf {
174        TypedPathBuf::from_unix(self)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn push_should_replace_current_path_with_provided_path_if_provided_path_is_absolute() {
184        // Empty current path will just become the provided path
185        let mut current_path = vec![];
186        UnixEncoding::push(&mut current_path, b"/abc");
187        assert_eq!(current_path, b"/abc");
188
189        // Non-empty relative current path will be replaced with the provided path
190        let mut current_path = b"some/path".to_vec();
191        UnixEncoding::push(&mut current_path, b"/abc");
192        assert_eq!(current_path, b"/abc");
193
194        // Non-empty absolute current path will be replaced with the provided path
195        let mut current_path = b"/some/path/".to_vec();
196        UnixEncoding::push(&mut current_path, b"/abc");
197        assert_eq!(current_path, b"/abc");
198    }
199
200    #[test]
201    fn push_should_append_path_to_current_path_with_a_separator_if_provided_path_is_relative() {
202        // Empty current path will just become the provided path
203        let mut current_path = vec![];
204        UnixEncoding::push(&mut current_path, b"abc");
205        assert_eq!(current_path, b"abc");
206
207        // Non-empty current path will have provided path appended
208        let mut current_path = b"some/path".to_vec();
209        UnixEncoding::push(&mut current_path, b"abc");
210        assert_eq!(current_path, b"some/path/abc");
211
212        // Non-empty current path ending in separator will have provided path appended without sep
213        let mut current_path = b"some/path/".to_vec();
214        UnixEncoding::push(&mut current_path, b"abc");
215        assert_eq!(current_path, b"some/path/abc");
216    }
217
218    #[test]
219    fn push_checked_should_fail_if_providing_an_absolute_path() {
220        // Empty current path will fail when pushing an absolute path
221        let mut current_path = vec![];
222        assert_eq!(
223            UnixEncoding::push_checked(&mut current_path, b"/abc"),
224            Err(CheckedPathError::UnexpectedRoot)
225        );
226        assert_eq!(current_path, b"");
227
228        // Non-empty relative current path will fail when pushing an absolute path
229        let mut current_path = b"some/path".to_vec();
230        assert_eq!(
231            UnixEncoding::push_checked(&mut current_path, b"/abc"),
232            Err(CheckedPathError::UnexpectedRoot)
233        );
234        assert_eq!(current_path, b"some/path");
235
236        // Non-empty absolute current path will fail when pushing an absolute path
237        let mut current_path = b"/some/path/".to_vec();
238        assert_eq!(
239            UnixEncoding::push_checked(&mut current_path, b"/abc"),
240            Err(CheckedPathError::UnexpectedRoot)
241        );
242        assert_eq!(current_path, b"/some/path/");
243    }
244
245    #[test]
246    fn push_checked_should_fail_if_providing_a_path_with_disallowed_filename_bytes() {
247        // Empty current path will fail when pushing a path containing disallowed filename bytes
248        let mut current_path = vec![];
249        assert_eq!(
250            UnixEncoding::push_checked(&mut current_path, b"some/inva\0lid/path"),
251            Err(CheckedPathError::InvalidFilename)
252        );
253        assert_eq!(current_path, b"");
254
255        // Non-empty relative current path will fail when pushing a path containing disallowed
256        // filename bytes
257        let mut current_path = b"some/path".to_vec();
258        assert_eq!(
259            UnixEncoding::push_checked(&mut current_path, b"some/inva\0lid/path"),
260            Err(CheckedPathError::InvalidFilename)
261        );
262        assert_eq!(current_path, b"some/path");
263
264        // Non-empty absolute current path will fail when pushing a path containing disallowed
265        // filename bytes
266        let mut current_path = b"/some/path/".to_vec();
267        assert_eq!(
268            UnixEncoding::push_checked(&mut current_path, b"some/inva\0lid/path"),
269            Err(CheckedPathError::InvalidFilename)
270        );
271        assert_eq!(current_path, b"/some/path/");
272    }
273
274    #[test]
275    fn push_checked_should_fail_if_providing_a_path_that_would_escape_the_current_path() {
276        // Empty current path will fail when pushing a path that would escape
277        let mut current_path = vec![];
278        assert_eq!(
279            UnixEncoding::push_checked(&mut current_path, b".."),
280            Err(CheckedPathError::PathTraversalAttack)
281        );
282        assert_eq!(current_path, b"");
283
284        // Non-empty relative current path will fail when pushing a path that would escape
285        let mut current_path = b"some/path".to_vec();
286        assert_eq!(
287            UnixEncoding::push_checked(&mut current_path, b".."),
288            Err(CheckedPathError::PathTraversalAttack)
289        );
290        assert_eq!(current_path, b"some/path");
291
292        // Non-empty absolute current path will fail when pushing a path that would escape
293        let mut current_path = b"/some/path/".to_vec();
294        assert_eq!(
295            UnixEncoding::push_checked(&mut current_path, b".."),
296            Err(CheckedPathError::PathTraversalAttack)
297        );
298        assert_eq!(current_path, b"/some/path/");
299    }
300
301    #[test]
302    fn push_checked_should_append_path_to_current_path_with_a_separator_if_does_not_violate_rules()
303    {
304        // Pushing a path that contains parent dirs, but does not escape the current path,
305        // should succeed
306        let mut current_path = vec![];
307        assert_eq!(
308            UnixEncoding::push_checked(&mut current_path, b"abc/../def/."),
309            Ok(()),
310        );
311        assert_eq!(current_path, b"abc/../def/.");
312
313        let mut current_path = b"some/path".to_vec();
314        assert_eq!(
315            UnixEncoding::push_checked(&mut current_path, b"abc/../def/."),
316            Ok(()),
317        );
318        assert_eq!(current_path, b"some/path/abc/../def/.");
319
320        let mut current_path = b"/some/path/".to_vec();
321        assert_eq!(
322            UnixEncoding::push_checked(&mut current_path, b"abc/../def/."),
323            Ok(()),
324        );
325        assert_eq!(current_path, b"/some/path/abc/../def/.");
326    }
327}