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