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