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::PathElementCS;
71/// assert!(PathElementCS::new("").is_err());
72/// assert!(PathElementCS::new(".").is_err());
73/// assert!(PathElementCS::new("..").is_err());
74/// assert!(PathElementCS::new("a/b").is_err());
75/// assert!(PathElementCS::new("hello.txt").is_ok());
76/// ```
77#[derive(Debug)]
78pub struct Error {
79    original: String,
80    kind: ErrorKind,
81}
82
83impl Error {
84    /// Returns the kind of error.
85    #[must_use]
86    pub fn kind(&self) -> &ErrorKind {
87        &self.kind
88    }
89
90    /// Consumes `self` and returns the [`ErrorKind`].
91    #[must_use]
92    pub fn into_kind(self) -> ErrorKind {
93        self.kind
94    }
95
96    /// Returns the original input string that caused the error.
97    #[must_use]
98    pub fn original(&self) -> &str {
99        &self.original
100    }
101}
102
103impl core::fmt::Display for Error {
104    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
105        if self.original.is_empty() {
106            write!(f, "{}", self.kind)
107        } else {
108            write!(f, "{}: {:?}", self.kind, self.original)
109        }
110    }
111}
112
113impl core::error::Error for Error {}
114
115/// A [`Result`](core::result::Result) type alias using this crate's [`Error`].
116pub type Result<T> = core::result::Result<T, Error>;
117
118/// A [`Result`](core::result::Result) type alias using [`ErrorKind`] directly.
119///
120/// Used by internal normalization functions that do not have access to the
121/// original input string. The [`PathElement`](crate::PathElementGeneric) constructors
122/// convert `ResultKind` into [`Result`] by attaching the original.
123pub type ResultKind<T> = core::result::Result<T, ErrorKind>;
124
125#[cfg(test)]
126mod tests {
127    use alloc::format;
128    use alloc::string::{String, ToString};
129
130    #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
131    use wasm_bindgen_test::wasm_bindgen_test as test;
132
133    use super::ErrorKind;
134
135    #[test]
136    fn error_kind_display() {
137        assert_eq!(ErrorKind::Empty.to_string(), "empty path element");
138        assert_eq!(
139            ErrorKind::CurrentDirectoryMarker.to_string(),
140            "current directory marker"
141        );
142        assert_eq!(
143            ErrorKind::ParentDirectoryMarker.to_string(),
144            "parent directory marker"
145        );
146        assert_eq!(
147            ErrorKind::ContainsForwardSlash.to_string(),
148            "contains forward slash"
149        );
150        assert_eq!(
151            ErrorKind::ContainsNullByte.to_string(),
152            "contains null byte"
153        );
154        assert_eq!(ErrorKind::InvalidUtf8.to_string(), "invalid UTF-8");
155        assert_eq!(
156            ErrorKind::ContainsUnassignedChar.to_string(),
157            "contains unassigned character"
158        );
159    }
160
161    #[test]
162    fn into_error_stores_original() {
163        let err = ErrorKind::Empty.into_error(String::from("  "));
164        assert_eq!(err.original(), "  ");
165        assert!(matches!(err.kind(), ErrorKind::Empty));
166    }
167
168    #[test]
169    fn into_error_empty_original() {
170        let err = ErrorKind::Empty.into_error(String::new());
171        assert_eq!(err.original(), "");
172        assert!(matches!(err.kind(), ErrorKind::Empty));
173    }
174
175    #[test]
176    fn into_kind_roundtrip() {
177        let err = ErrorKind::ContainsNullByte.into_error(String::from("a\0b"));
178        assert!(matches!(err.into_kind(), ErrorKind::ContainsNullByte));
179    }
180
181    #[test]
182    fn error_display_with_original() {
183        let err = ErrorKind::ContainsForwardSlash.into_error(String::from("a/b"));
184        assert_eq!(format!("{err}"), "contains forward slash: \"a/b\"");
185    }
186
187    #[test]
188    fn error_display_empty_original() {
189        let err = ErrorKind::Empty.into_error(String::new());
190        assert_eq!(format!("{err}"), "empty path element");
191    }
192
193    #[test]
194    fn error_debug() {
195        let err = ErrorKind::Empty.into_error(String::from("."));
196        let debug = format!("{err:?}");
197        assert!(debug.contains("Empty"));
198        assert!(debug.contains('.'));
199    }
200
201    #[test]
202    fn path_element_error_has_original() {
203        let err = crate::PathElementCS::new("a/b").unwrap_err();
204        assert!(matches!(err.kind(), ErrorKind::ContainsForwardSlash));
205        assert_eq!(err.original(), "a/b");
206    }
207
208    #[test]
209    fn path_element_error_empty() {
210        let err = crate::PathElementCS::new("").unwrap_err();
211        assert!(matches!(err.kind(), ErrorKind::Empty));
212        assert_eq!(err.original(), "");
213    }
214
215    #[test]
216    fn path_element_error_dot() {
217        let err = crate::PathElementCI::new(".").unwrap_err();
218        assert!(matches!(err.kind(), ErrorKind::CurrentDirectoryMarker));
219        assert_eq!(err.original(), ".");
220    }
221
222    #[test]
223    fn path_element_error_dotdot() {
224        let err = crate::PathElementCS::new("..").unwrap_err();
225        assert!(matches!(err.kind(), ErrorKind::ParentDirectoryMarker));
226        assert_eq!(err.original(), "..");
227    }
228
229    #[test]
230    fn path_element_error_null_byte() {
231        let err = crate::PathElementCS::new("a\0b").unwrap_err();
232        assert!(matches!(err.kind(), ErrorKind::ContainsNullByte));
233        assert_eq!(err.original(), "a\0b");
234    }
235
236    #[test]
237    fn path_element_error_whitespace_trimmed_to_empty() {
238        let err = crate::PathElementCS::new("   ").unwrap_err();
239        assert!(matches!(err.kind(), ErrorKind::Empty));
240        assert_eq!(err.original(), "   ");
241    }
242
243    #[test]
244    fn path_element_error_bom_trimmed_to_dot() {
245        let err = crate::PathElementCS::new("\u{FEFF}.").unwrap_err();
246        assert!(matches!(err.kind(), ErrorKind::CurrentDirectoryMarker));
247        assert_eq!(err.original(), "\u{FEFF}.");
248    }
249}