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