Skip to main content

normalized_path/
error.rs

1use alloc::string::String;
2
3/// The kind of error that occurred during path element normalization or validation.
4///
5/// See [`Error`] for the full error type, which also carries the original input string.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum ErrorKind {
8    /// The name is empty (or becomes empty after whitespace trimming).
9    Empty,
10
11    /// The name is `.`, the current directory marker.
12    CurrentDirectoryMarker,
13
14    /// The name is `..`, the parent directory marker.
15    ParentDirectoryMarker,
16
17    /// The name contains a forward slash (`/`), which is a path separator.
18    ContainsForwardSlash,
19
20    /// The name contains a null byte (`\0`), which all OSes treat as a string
21    /// terminator, silently truncating the name.
22    ContainsNullByte,
23
24    /// The byte input is not valid UTF-8.
25    InvalidUtf8,
26
27    /// The name contains a Unicode code point that is not assigned in the
28    /// version of Unicode used by this crate.
29    ContainsUnassignedChar,
30
31    /// Apple's `CFStringGetFileSystemRepresentation` failed.
32    /// This should never occur in practice, since validation runs before this.
33    #[cfg(target_vendor = "apple")]
34    GetFileSystemRepresentationError,
35}
36
37impl ErrorKind {
38    /// Converts this error kind into an [`Error`], attaching the original input string.
39    pub(crate) fn into_error(self, original: impl Into<String>) -> Error {
40        Error {
41            original: original.into(),
42            kind: self,
43        }
44    }
45}
46
47impl core::fmt::Display for ErrorKind {
48    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
49        match self {
50            Self::Empty => f.write_str("empty path element"),
51            Self::CurrentDirectoryMarker => f.write_str("current directory marker"),
52            Self::ParentDirectoryMarker => f.write_str("parent directory marker"),
53            Self::ContainsForwardSlash => f.write_str("contains forward slash"),
54            Self::ContainsNullByte => f.write_str("contains null byte"),
55            Self::InvalidUtf8 => f.write_str("invalid UTF-8"),
56            Self::ContainsUnassignedChar => f.write_str("contains unassigned character"),
57            #[cfg(target_vendor = "apple")]
58            Self::GetFileSystemRepresentationError => {
59                f.write_str("CFStringGetFileSystemRepresentation failed")
60            }
61        }
62    }
63}
64
65/// An error that occurred during path element normalization or validation.
66///
67/// Contains the [`ErrorKind`] and the original input string that caused the error.
68///
69/// ```
70/// use normalized_path::{ErrorKind, PathElementCS};
71///
72/// assert!(PathElementCS::new("hello.txt").is_ok());
73///
74/// assert_eq!(PathElementCS::new("a/b").unwrap_err().original(), "a/b");
75///
76/// assert_eq!(PathElementCS::new("").unwrap_err().kind(), ErrorKind::Empty);
77/// assert_eq!(PathElementCS::new(".").unwrap_err().kind(), ErrorKind::CurrentDirectoryMarker);
78/// assert_eq!(PathElementCS::new("..").unwrap_err().kind(), ErrorKind::ParentDirectoryMarker);
79/// assert_eq!(PathElementCS::new("a/b").unwrap_err().kind(), ErrorKind::ContainsForwardSlash);
80/// assert_eq!(PathElementCS::new("a\0b").unwrap_err().kind(), ErrorKind::ContainsNullByte);
81/// assert_eq!(PathElementCS::from_bytes(b"\xff").unwrap_err().kind(), ErrorKind::InvalidUtf8);
82/// assert_eq!(PathElementCS::new("\u{0378}").unwrap_err().kind(), ErrorKind::ContainsUnassignedChar);
83/// ```
84#[derive(Debug)]
85pub struct Error {
86    original: String,
87    kind: ErrorKind,
88}
89
90impl Error {
91    /// Returns the kind of error.
92    #[must_use]
93    pub fn kind(&self) -> ErrorKind {
94        self.kind
95    }
96
97    /// Returns the original input string that caused the error.
98    #[must_use]
99    pub fn original(&self) -> &str {
100        &self.original
101    }
102
103    /// Consumes `self` and returns the original input string.
104    #[must_use]
105    pub fn into_original(self) -> String {
106        self.original
107    }
108}
109
110impl core::fmt::Display for Error {
111    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112        if self.original.is_empty() {
113            write!(f, "{}", self.kind)
114        } else {
115            write!(f, "{}: {:?}", self.kind, self.original)
116        }
117    }
118}
119
120impl core::error::Error for Error {}
121
122/// A [`Result`](core::result::Result) type alias using this crate's [`Error`].
123pub type Result<T> = core::result::Result<T, Error>;
124
125/// A [`Result`](core::result::Result) type alias using [`ErrorKind`] directly.
126///
127/// Used by internal normalization functions that do not have access to the
128/// original input string. The [`PathElement`](crate::PathElementGeneric) constructors
129/// convert `ResultKind` into [`Result`] by attaching the original.
130pub type ResultKind<T> = core::result::Result<T, ErrorKind>;
131
132#[cfg(test)]
133mod tests {
134    use alloc::format;
135    use alloc::string::{String, ToString};
136
137    #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
138    use wasm_bindgen_test::wasm_bindgen_test as test;
139
140    use super::ErrorKind;
141
142    #[test]
143    fn error_kind_display() {
144        assert_eq!(ErrorKind::Empty.to_string(), "empty path element");
145        assert_eq!(
146            ErrorKind::CurrentDirectoryMarker.to_string(),
147            "current directory marker"
148        );
149        assert_eq!(
150            ErrorKind::ParentDirectoryMarker.to_string(),
151            "parent directory marker"
152        );
153        assert_eq!(
154            ErrorKind::ContainsForwardSlash.to_string(),
155            "contains forward slash"
156        );
157        assert_eq!(
158            ErrorKind::ContainsNullByte.to_string(),
159            "contains null byte"
160        );
161        assert_eq!(ErrorKind::InvalidUtf8.to_string(), "invalid UTF-8");
162        assert_eq!(
163            ErrorKind::ContainsUnassignedChar.to_string(),
164            "contains unassigned character"
165        );
166    }
167
168    #[test]
169    fn into_error_stores_original() {
170        let err = ErrorKind::Empty.into_error(String::from("  "));
171        assert_eq!(err.kind(), ErrorKind::Empty);
172        assert_eq!(err.original(), "  ");
173    }
174
175    #[test]
176    fn into_error_empty_original() {
177        let err = ErrorKind::Empty.into_error(String::new());
178        assert_eq!(err.kind(), ErrorKind::Empty);
179        assert_eq!(err.original(), "");
180    }
181
182    #[test]
183    fn kind_roundtrip() {
184        let err = ErrorKind::ContainsNullByte.into_error(String::from("a\0b"));
185        assert_eq!(err.kind(), ErrorKind::ContainsNullByte);
186    }
187
188    #[test]
189    fn into_original() {
190        let err = ErrorKind::Empty.into_error(String::from("  "));
191        assert_eq!(err.into_original(), "  ");
192    }
193
194    #[test]
195    fn error_display_with_original() {
196        let err = ErrorKind::ContainsForwardSlash.into_error(String::from("a/b"));
197        assert_eq!(format!("{err}"), "contains forward slash: \"a/b\"");
198    }
199
200    #[test]
201    fn error_display_empty_original() {
202        let err = ErrorKind::Empty.into_error(String::new());
203        assert_eq!(format!("{err}"), "empty path element");
204    }
205
206    #[test]
207    fn error_debug() {
208        let err = ErrorKind::Empty.into_error(String::from("."));
209        let debug = format!("{err:?}");
210        assert!(debug.contains("Empty"));
211        assert!(debug.contains('.'));
212    }
213
214    #[test]
215    fn path_element_error_has_original() {
216        let err = crate::PathElementCS::new("a/b").unwrap_err();
217        assert_eq!(err.kind(), ErrorKind::ContainsForwardSlash);
218        assert_eq!(err.original(), "a/b");
219    }
220
221    #[test]
222    fn path_element_error_empty() {
223        let err = crate::PathElementCS::new("").unwrap_err();
224        assert_eq!(err.kind(), ErrorKind::Empty);
225        assert_eq!(err.original(), "");
226    }
227
228    #[test]
229    fn path_element_error_dot() {
230        let err = crate::PathElementCI::new(".").unwrap_err();
231        assert_eq!(err.kind(), ErrorKind::CurrentDirectoryMarker);
232        assert_eq!(err.original(), ".");
233    }
234
235    #[test]
236    fn path_element_error_dotdot() {
237        let err = crate::PathElementCS::new("..").unwrap_err();
238        assert_eq!(err.kind(), ErrorKind::ParentDirectoryMarker);
239        assert_eq!(err.original(), "..");
240    }
241
242    #[test]
243    fn path_element_error_null_byte() {
244        let err = crate::PathElementCS::new("a\0b").unwrap_err();
245        assert_eq!(err.kind(), ErrorKind::ContainsNullByte);
246        assert_eq!(err.original(), "a\0b");
247    }
248
249    #[test]
250    fn path_element_error_whitespace_trimmed_to_empty() {
251        let err = crate::PathElementCS::new("   ").unwrap_err();
252        assert_eq!(err.kind(), ErrorKind::Empty);
253        assert_eq!(err.original(), "   ");
254    }
255
256    #[test]
257    fn path_element_error_bom_trimmed_to_dot() {
258        let err = crate::PathElementCS::new("\u{FEFF}.").unwrap_err();
259        assert_eq!(err.kind(), ErrorKind::CurrentDirectoryMarker);
260        assert_eq!(err.original(), "\u{FEFF}.");
261    }
262}