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}