scolapasta_string_escape/
string.rs

1use core::fmt::{self, Write};
2
3use crate::literal::{Literal, ascii_char_with_escape};
4
5/// Write a UTF-8 debug representation of a byte slice into the given writer.
6///
7/// This method encodes a bytes slice into a UTF-8 valid representation by
8/// writing invalid sequences as hex escape codes (e.g. `\x00`) or C escape
9/// sequences (e.g. `\a`).
10///
11/// This method also escapes UTF-8 valid characters like `\n` and `\t`.
12///
13/// # Examples
14///
15/// Basic usage:
16///
17/// ```
18/// # use scolapasta_string_escape::format_debug_escape_into;
19///
20/// let mut message = String::from("cannot load such file -- ");
21/// let filename = b"utf8-invalid-name-\xFF";
22/// format_debug_escape_into(&mut message, filename);
23/// assert_eq!(r"cannot load such file -- utf8-invalid-name-\xFF", message);
24/// ```
25///
26/// # Errors
27///
28/// This method only returns an error when the given writer returns an
29/// error.
30pub fn format_debug_escape_into<W, T>(mut dest: W, message: T) -> fmt::Result
31where
32    W: Write,
33    T: AsRef<[u8]>,
34{
35    let mut buf = [0; 4];
36    let mut message = message.as_ref();
37    while !message.is_empty() {
38        let (ch, size) = bstr::decode_utf8(message);
39        match ch.map(|ch| ascii_char_with_escape(ch).ok_or(ch)) {
40            Some(Ok(escaped)) => {
41                dest.write_str(escaped)?;
42            }
43            Some(Err(ch)) => {
44                let enc = ch.encode_utf8(&mut buf);
45                dest.write_str(enc)?;
46            }
47            // Otherwise, we've gotten invalid UTF-8, which means this is not an
48            // printable char.
49            None => {
50                for &byte in &message[..size] {
51                    let escaped = Literal::debug_escape(byte);
52                    dest.write_str(escaped)?;
53                }
54            }
55        }
56        message = &message[size..];
57    }
58    Ok(())
59}
60
61#[cfg(test)]
62mod tests {
63    use alloc::string::{String, ToString};
64
65    use super::format_debug_escape_into;
66
67    #[test]
68    fn format_ascii_message() {
69        let message = "Spinoso Exception";
70        let mut dest = String::new();
71        format_debug_escape_into(&mut dest, message).unwrap();
72        assert_eq!(dest, "Spinoso Exception");
73    }
74
75    #[test]
76    fn format_unicode_message() {
77        let message = "Spinoso Exception 💎🦀";
78        let mut dest = String::new();
79        format_debug_escape_into(&mut dest, message).unwrap();
80        assert_eq!(dest, "Spinoso Exception 💎🦀");
81    }
82
83    #[test]
84    fn format_invalid_utf8_message() {
85        let message = b"oh no! \xFF";
86        let mut dest = String::new();
87        format_debug_escape_into(&mut dest, message).unwrap();
88        assert_eq!(dest, r"oh no! \xFF");
89    }
90
91    #[test]
92    fn format_escape_code_message() {
93        let message = "yes to symbolic \t\n\x7F";
94        let mut dest = String::new();
95        format_debug_escape_into(&mut dest, message).unwrap();
96        assert_eq!(dest, r"yes to symbolic \t\n\x7F");
97    }
98
99    #[test]
100    fn replacement_character() {
101        let message = "This is the replacement character: \u{FFFD}";
102        let mut dest = String::new();
103        format_debug_escape_into(&mut dest, message).unwrap();
104        assert_eq!(dest, "This is the replacement character: \u{FFFD}");
105    }
106
107    #[test]
108    fn as_ref() {
109        let message = b"Danger".to_vec();
110        let mut dest = String::new();
111        format_debug_escape_into(&mut dest, message).unwrap();
112        assert_eq!(dest, "Danger");
113
114        let message = "Danger".to_string();
115        let mut dest = String::new();
116        format_debug_escape_into(&mut dest, message).unwrap();
117        assert_eq!(dest, "Danger");
118    }
119}