zalgo_codec_common/error.rs
1//! Contains the definition of the error type used by the encoding functions in the crate.
2
3use core::{fmt, str::Utf8Error};
4
5#[cfg(feature = "std")]
6use std::backtrace::Backtrace;
7
8use alloc::string::FromUtf8Error;
9
10#[derive(Debug)]
11/// The error returned by [`zalgo_encode`](crate::zalgo_encode), [`ZalgoString::new`](crate::ZalgoString::new), and [`zalgo_wrap_python`](crate::zalgo_wrap_python)
12/// if they encounter a byte they can not encode.
13pub struct EncodeError {
14 unencodable_character: char,
15 line: usize,
16 column: usize,
17 index: usize,
18 #[cfg(feature = "std")]
19 backtrace: Backtrace,
20}
21
22impl EncodeError {
23 /// Creates a new `Error`.
24 ///
25 /// # Note
26 ///
27 /// This associated method does not check the validity of its inputs,
28 /// and just constructs a new `Error` instance.
29 #[inline]
30 #[must_use = "this associated method does not modify its inputs and just returns a new value"]
31 pub(crate) fn new(
32 unencodable_character: char,
33 line: usize,
34 column: usize,
35 index: usize,
36 ) -> Self {
37 Self {
38 unencodable_character,
39 line,
40 column,
41 index,
42 #[cfg(feature = "std")]
43 backtrace: Backtrace::capture(),
44 }
45 }
46
47 /// Returns the 1-indexed line number of the line on which the unencodable byte occured.
48 ///
49 /// # Examples
50 ///
51 /// ```
52 /// # use zalgo_codec_common::{EncodeError, zalgo_encode};
53 /// assert_eq!(zalgo_encode("❤️").map_err(|e| e.line()), Err(1));
54 /// assert_eq!(zalgo_encode("a\nb\nc\r\n").map_err(|e| e.line()), Err(3));
55 /// ```
56 #[inline]
57 #[must_use = "the method returns a new value and does not modify `self`"]
58 pub const fn line(&self) -> usize {
59 self.line
60 }
61
62 /// Returns the 1-indexed column where the unencodable byte occured.
63 /// Columns are counted from left to right and the count resets for each new line.
64 ///
65 /// # Example
66 ///
67 /// ```
68 /// # use zalgo_codec_common::{EncodeError, zalgo_encode};
69 /// assert_eq!(zalgo_encode("I ❤️ 🎂").map_err(|e| e.column()), Err(3));
70 /// assert_eq!(zalgo_encode("I\n❤️\n🎂").map_err(|e| e.column()), Err(1));
71 /// ```
72 #[inline]
73 #[must_use = "the method returns a new value and does not modify `self`"]
74 pub const fn column(&self) -> usize {
75 self.column
76 }
77
78 /// Returns the unencodable character that caused the error.
79 ///
80 /// This may not match with what you see when you look at the unencoded string in a text editor since
81 /// some grapheme clusters consist of many unicode characters.
82 ///
83 /// # Examples
84 ///
85 /// ```
86 /// # use zalgo_codec_common::zalgo_encode;
87 /// assert_eq!(zalgo_encode("CRLF\r\n").map_err(|e| e.char()), Err('\r'));
88 /// ```
89 /// The ❤️ emoji consists of two characters, the heart `U+2764` and the color variant selector `U+FE0F`.
90 /// Since the heart is not encodable, that is the place where the error is generated:
91 /// ```
92 /// # use zalgo_codec_common::zalgo_encode;
93 /// assert_eq!(zalgo_encode("❤️").map_err(|e| e.char()), Err('❤'));
94 /// ```
95 /// The grapheme cluster `á` consists of a normal `a` and a combining acute accent, `U+301`.
96 /// The `a` can be encoded and the combining acute accent can not, so the error points only to the accent:
97 /// ```
98 /// # use zalgo_codec_common::zalgo_encode;
99 /// assert_eq!(zalgo_encode("á").map_err(|e| e.char()), Err('\u{301}'))
100 /// ```
101 #[inline]
102 #[must_use = "the method returns a new value and does not modify `self`"]
103 pub const fn char(&self) -> char {
104 self.unencodable_character
105 }
106
107 /// Returns the index of the string where the unencodable character occured.
108 ///
109 /// # Example
110 ///
111 /// ```
112 /// # use zalgo_codec_common::zalgo_encode;
113 /// assert_eq!(zalgo_encode("ab\ncdë").map_err(|e| e.index()), Err(5));
114 /// ```
115 #[inline]
116 #[must_use = "the method returns a new value and does not modify `self`"]
117 pub const fn index(&self) -> usize {
118 self.index
119 }
120
121 #[cfg(feature = "std")]
122 /// Returns a reference to a [`Backtrace`] that was captured when the error was created.
123 ///
124 /// See the documentation of [`Backtrace::capture`] for more information about how to make it
125 /// show more information when displayed.
126 #[inline]
127 pub fn backtrace(&self) -> &Backtrace {
128 &self.backtrace
129 }
130}
131
132impl fmt::Display for EncodeError {
133 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
134 write!(
135 f,
136 "can not encode {:?} character at string index {}, on line {} at column {}",
137 self.char(),
138 self.index(),
139 self.line(),
140 self.column(),
141 )
142 }
143}
144
145impl core::error::Error for EncodeError {}
146
147/// The error returned by [`zalgo_decode`](super::zalgo_decode) if a string can not be decoded.
148#[derive(Debug)]
149pub struct DecodeError {
150 kind: DecodeErrorKind,
151 #[cfg(feature = "std")]
152 backtrace: Backtrace,
153}
154
155impl DecodeError {
156 pub(crate) fn new(possible_error: Option<FromUtf8Error>) -> Self {
157 Self {
158 #[cfg(feature = "std")]
159 backtrace: Backtrace::capture(),
160 kind: match possible_error {
161 Some(e) => DecodeErrorKind::InvalidUtf8(e),
162 None => DecodeErrorKind::EmptyInput,
163 },
164 }
165 }
166
167 /// Returns whether the error happened because the given string was empty,
168 /// and not because the decoding resulted in invalid UTF-8.
169 pub fn cause_was_empty_string(&self) -> bool {
170 matches!(self.kind, DecodeErrorKind::EmptyInput)
171 }
172
173 #[cfg(feature = "std")]
174 /// Returns a backtrace to where the error was created.
175 ///
176 /// The backtrace was captured with [`Backtrace::capture`], see it for more information
177 /// on how to make it show information when printed.
178 pub fn backtrace(&self) -> &Backtrace {
179 &self.backtrace
180 }
181
182 /// If the error happened because the decoding resulted in invalid UTF-8,
183 /// this function returns the [`Utf8Error`] that was created in the process.
184 pub fn to_utf8_error(&self) -> Option<Utf8Error> {
185 match &self.kind {
186 DecodeErrorKind::InvalidUtf8(e) => Some(e.utf8_error()),
187 DecodeErrorKind::EmptyInput => None,
188 }
189 }
190
191 /// If the error happened because the decoding resulted in invalid UTF-8,
192 /// this function converts this error into the [`FromUtf8Error`] that was created in the process.
193 pub fn into_from_utf8_error(self) -> Option<FromUtf8Error> {
194 match self.kind {
195 DecodeErrorKind::InvalidUtf8(e) => Some(e),
196 DecodeErrorKind::EmptyInput => None,
197 }
198 }
199}
200
201impl fmt::Display for DecodeError {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 write!(f, "could not decode the string because {}", self.kind)
204 }
205}
206
207impl core::error::Error for DecodeError {
208 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
209 match self.kind {
210 DecodeErrorKind::InvalidUtf8(ref e) => Some(e),
211 DecodeErrorKind::EmptyInput => None,
212 }
213 }
214}
215
216/// The kind of error the caused the decoding failure.
217#[derive(Debug, Clone, PartialEq, Eq)]
218enum DecodeErrorKind {
219 /// The given string was empty.
220 EmptyInput,
221 /// Decoding the string resulted in invalid UTF-8.
222 InvalidUtf8(FromUtf8Error),
223}
224
225impl fmt::Display for DecodeErrorKind {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 match self {
228 Self::EmptyInput => write!(f, "the string was empty"),
229 Self::InvalidUtf8(e) => write!(f, "decoding resulted in invalid utf8: {e}"),
230 }
231 }
232}
233
234#[cfg(test)]
235mod test {
236 use super::{DecodeError, EncodeError};
237 use alloc::{string::String, vec};
238
239 #[test]
240 fn test_error() {
241 let err = EncodeError::new('å', 1, 7, 6);
242 assert_eq!(err.char(), 'å');
243 assert_eq!(err.line(), 1);
244 assert_eq!(err.column(), 7);
245 assert_eq!(err.index(), 6);
246 }
247
248 #[test]
249 fn test_decode_error() {
250 let err = DecodeError::new(None);
251 assert_eq!(err.to_utf8_error(), None);
252 assert!(err.cause_was_empty_string());
253 assert_eq!(err.into_from_utf8_error(), None);
254 let err = DecodeError::new(String::from_utf8(vec![255, 255, 255, 255, 255, 255]).err());
255 assert_eq!(err.to_utf8_error().unwrap().error_len(), Some(1));
256 assert_eq!(err.to_utf8_error().unwrap().valid_up_to(), 0);
257 assert!(!err.cause_was_empty_string());
258 assert_eq!(
259 err.into_from_utf8_error().unwrap().into_bytes(),
260 vec![255; 6]
261 );
262 }
263}