imap_types/
utils.rs

1//! Functions that may come in handy.
2
3use std::borrow::Cow;
4
5/// Converts bytes into a ready-to-be-printed form.
6pub fn escape_byte_string<B>(bytes: B) -> String
7where
8    B: AsRef<[u8]>,
9{
10    let bytes = bytes.as_ref();
11
12    bytes
13        .iter()
14        .map(|byte| match byte {
15            0x00..=0x08 => format!("\\x{:02x}", byte),
16            0x09 => String::from("\\t"),
17            0x0A => String::from("\\n"),
18            0x0B => format!("\\x{:02x}", byte),
19            0x0C => format!("\\x{:02x}", byte),
20            0x0D => String::from("\\r"),
21            0x0e..=0x1f => format!("\\x{:02x}", byte),
22            0x20..=0x21 => format!("{}", *byte as char),
23            0x22 => String::from("\\\""),
24            0x23..=0x5B => format!("{}", *byte as char),
25            0x5C => String::from("\\\\"),
26            0x5D..=0x7E => format!("{}", *byte as char),
27            0x7f => format!("\\x{:02x}", byte),
28            0x80..=0xff => format!("\\x{:02x}", byte),
29        })
30        .collect::<Vec<String>>()
31        .join("")
32}
33
34pub mod indicators {
35    /// Any 7-bit US-ASCII character, excluding NUL
36    ///
37    /// CHAR = %x01-7F
38    pub fn is_char(byte: u8) -> bool {
39        matches!(byte, 0x01..=0x7f)
40    }
41
42    /// Controls
43    ///
44    /// CTL = %x00-1F / %x7F
45    pub fn is_ctl(byte: u8) -> bool {
46        matches!(byte, 0x00..=0x1f | 0x7f)
47    }
48
49    pub(crate) fn is_any_text_char_except_quoted_specials(byte: u8) -> bool {
50        is_text_char(byte) && !is_quoted_specials(byte)
51    }
52
53    /// `quoted-specials = DQUOTE / "\"`
54    pub fn is_quoted_specials(byte: u8) -> bool {
55        byte == b'"' || byte == b'\\'
56    }
57
58    /// `ASTRING-CHAR = ATOM-CHAR / resp-specials`
59    pub fn is_astring_char(i: u8) -> bool {
60        is_atom_char(i) || is_resp_specials(i)
61    }
62
63    /// `ATOM-CHAR = <any CHAR except atom-specials>`
64    pub fn is_atom_char(b: u8) -> bool {
65        is_char(b) && !is_atom_specials(b)
66    }
67
68    /// `atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials / resp-specials`
69    pub fn is_atom_specials(i: u8) -> bool {
70        match i {
71            b'(' | b')' | b'{' | b' ' => true,
72            c if is_ctl(c) => true,
73            c if is_list_wildcards(c) => true,
74            c if is_quoted_specials(c) => true,
75            c if is_resp_specials(c) => true,
76            _ => false,
77        }
78    }
79
80    /// `list-wildcards = "%" / "*"`
81    pub fn is_list_wildcards(i: u8) -> bool {
82        i == b'%' || i == b'*'
83    }
84
85    #[inline]
86    /// `resp-specials = "]"`
87    pub fn is_resp_specials(i: u8) -> bool {
88        i == b']'
89    }
90
91    #[inline]
92    /// `CHAR8 = %x01-ff`
93    ///
94    /// Any OCTET except NUL, %x00
95    pub fn is_char8(i: u8) -> bool {
96        i != 0
97    }
98
99    /// `TEXT-CHAR = %x01-09 / %x0B-0C / %x0E-7F`
100    ///
101    /// Note: This was `<any CHAR except CR and LF>` before.
102    pub fn is_text_char(c: u8) -> bool {
103        matches!(c, 0x01..=0x09 | 0x0b..=0x0c | 0x0e..=0x7f)
104    }
105
106    /// `list-char = ATOM-CHAR / list-wildcards / resp-specials`
107    pub fn is_list_char(i: u8) -> bool {
108        is_atom_char(i) || is_list_wildcards(i) || is_resp_specials(i)
109    }
110}
111
112pub fn escape_quoted(unescaped: &str) -> Cow<str> {
113    let mut escaped = Cow::Borrowed(unescaped);
114
115    if escaped.contains('\\') {
116        escaped = Cow::Owned(escaped.replace('\\', "\\\\"));
117    }
118
119    if escaped.contains('\"') {
120        escaped = Cow::Owned(escaped.replace('"', "\\\""));
121    }
122
123    escaped
124}
125
126pub fn unescape_quoted(escaped: &str) -> Cow<str> {
127    let mut unescaped = Cow::Borrowed(escaped);
128
129    if unescaped.contains("\\\\") {
130        unescaped = Cow::Owned(unescaped.replace("\\\\", "\\"));
131    }
132
133    if unescaped.contains("\\\"") {
134        unescaped = Cow::Owned(unescaped.replace("\\\"", "\""));
135    }
136
137    unescaped
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_escape_quoted() {
146        let tests = [
147            ("", ""),
148            ("\\", "\\\\"),
149            ("\"", "\\\""),
150            ("alice", "alice"),
151            ("\\alice\\", "\\\\alice\\\\"),
152            ("alice\"", "alice\\\""),
153            (r#"\alice\ ""#, r#"\\alice\\ \""#),
154        ];
155
156        for (test, expected) in tests {
157            let got = escape_quoted(test);
158            assert_eq!(expected, got);
159        }
160    }
161
162    #[test]
163    fn test_unescape_quoted() {
164        let tests = [
165            ("", ""),
166            ("\\\\", "\\"),
167            ("\\\"", "\""),
168            ("alice", "alice"),
169            ("\\\\alice\\\\", "\\alice\\"),
170            ("alice\\\"", "alice\""),
171            (r#"\\alice\\ \""#, r#"\alice\ ""#),
172        ];
173
174        for (test, expected) in tests {
175            let got = unescape_quoted(test);
176            assert_eq!(expected, got);
177        }
178    }
179
180    #[test]
181    fn test_that_unescape_is_inverse_of_escape() {
182        let input = "\\\"\\¹²³abc_*:;059^$%§!\"";
183
184        assert_eq!(input, unescape_quoted(escape_quoted(input).as_ref()));
185    }
186
187    #[test]
188    fn test_escape_byte_string() {
189        for byte in 0u8..=255 {
190            let got = escape_byte_string([byte]);
191
192            if byte.is_ascii_alphanumeric() {
193                assert_eq!((byte as char).to_string(), got.to_string());
194            } else if byte.is_ascii_whitespace() {
195                if byte == b'\t' {
196                    assert_eq!(String::from("\\t"), got);
197                } else if byte == b'\n' {
198                    assert_eq!(String::from("\\n"), got);
199                }
200            } else if byte.is_ascii_punctuation() {
201                if byte == b'\\' {
202                    assert_eq!(String::from("\\\\"), got);
203                } else if byte == b'"' {
204                    assert_eq!(String::from("\\\""), got);
205                } else {
206                    assert_eq!((byte as char).to_string(), got);
207                }
208            } else {
209                assert_eq!(format!("\\x{:02x}", byte), got);
210            }
211        }
212
213        let tests = [(b"Hallo \"\\\x00", String::from(r#"Hallo \"\\\x00"#))];
214
215        for (test, expected) in tests {
216            let got = escape_byte_string(test);
217            assert_eq!(expected, got);
218        }
219    }
220}