Skip to main content

contextual_encoder/
rust.rs

1//! rust literal encoders.
2//!
3//! encodes untrusted strings for safe embedding in rust source literals.
4//!
5//! - [`for_rust_string`] — safe for rust string literals (`"..."`)
6//! - [`for_rust_char`] — safe for rust char literals (`'...'`)
7//! - [`for_rust_byte_string`] — safe for rust byte string literals (`b"..."`)
8//!
9//! # encoding rules
10//!
11//! all three encoders use rust's native escape syntax:
12//!
13//! - named escapes: `\0`, `\t`, `\n`, `\r`, `\\`
14//! - C0 controls and DEL without named escapes → `\xHH`
15//! - unicode non-characters → space (string/char) or `\xHH` per byte (byte string)
16//!
17//! the encoders differ in which quote is escaped and how non-ASCII is handled:
18//!
19//! | encoder | quote escape | non-ASCII |
20//! |---------|-------------|-----------|
21//! | `for_rust_string` | `"` → `\"` | passes through |
22//! | `for_rust_char` | `'` → `\'` | passes through |
23//! | `for_rust_byte_string` | `"` → `\"` | each UTF-8 byte → `\xHH` |
24
25use std::fmt;
26
27use crate::engine::{
28    encode_loop, is_unicode_noncharacter, needs_byte_string_encoding, write_byte_string_encoded,
29    write_rust_named_escape,
30};
31
32/// encodes `input` for safe embedding in a rust string literal (`"..."`).
33///
34/// escapes backslashes, double quotes, and control characters using rust's
35/// escape syntax. non-ASCII unicode passes through unchanged (valid in rust
36/// string literals). unicode non-characters are replaced with space.
37///
38/// # examples
39///
40/// ```
41/// use contextual_encoder::for_rust_string;
42///
43/// assert_eq!(for_rust_string(r#"say "hi""#), r#"say \"hi\""#);
44/// assert_eq!(for_rust_string("line\nbreak"), r"line\nbreak");
45/// assert_eq!(for_rust_string("café"), "café");
46/// ```
47pub fn for_rust_string(input: &str) -> String {
48    let mut out = String::with_capacity(input.len());
49    write_rust_string(&mut out, input).expect("writing to string cannot fail");
50    out
51}
52
53/// writes the rust-string-encoded form of `input` to `out`.
54///
55/// see [`for_rust_string`] for encoding rules.
56pub fn write_rust_string<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
57    encode_loop(out, input, needs_rust_string_encoding, |out, c, _next| {
58        write_rust_text_encoded(out, c, '"')
59    })
60}
61
62fn needs_rust_string_encoding(c: char) -> bool {
63    matches!(c, '\x00'..='\x1F' | '\x7F' | '"' | '\\') || is_unicode_noncharacter(c as u32)
64}
65
66/// encodes `input` for safe embedding in a rust char literal (`'...'`).
67///
68/// escapes backslashes, single quotes, and control characters using rust's
69/// escape syntax. non-ASCII unicode passes through unchanged. unicode
70/// non-characters are replaced with space.
71///
72/// # examples
73///
74/// ```
75/// use contextual_encoder::for_rust_char;
76///
77/// assert_eq!(for_rust_char("it's"), r"it\'s");
78/// assert_eq!(for_rust_char(r#"a"b"#), r#"a"b"#);
79/// assert_eq!(for_rust_char("tab\there"), r"tab\there");
80/// ```
81pub fn for_rust_char(input: &str) -> String {
82    let mut out = String::with_capacity(input.len());
83    write_rust_char(&mut out, input).expect("writing to string cannot fail");
84    out
85}
86
87/// writes the rust-char-encoded form of `input` to `out`.
88///
89/// see [`for_rust_char`] for encoding rules.
90pub fn write_rust_char<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
91    encode_loop(out, input, needs_rust_char_encoding, |out, c, _next| {
92        write_rust_text_encoded(out, c, '\'')
93    })
94}
95
96fn needs_rust_char_encoding(c: char) -> bool {
97    matches!(c, '\x00'..='\x1F' | '\x7F' | '\'' | '\\') || is_unicode_noncharacter(c as u32)
98}
99
100/// writes the encoded form of a character for rust string or char context.
101/// `quote` is the delimiter being escaped (`"` or `'`).
102fn write_rust_text_encoded<W: fmt::Write>(out: &mut W, c: char, quote: char) -> fmt::Result {
103    match c {
104        '\0' => out.write_str("\\0"),
105        '\t' => out.write_str("\\t"),
106        '\n' => out.write_str("\\n"),
107        '\r' => out.write_str("\\r"),
108        '\\' => out.write_str("\\\\"),
109        '"' if quote == '"' => out.write_str("\\\""),
110        '\'' if quote == '\'' => out.write_str("\\'"),
111        c if is_unicode_noncharacter(c as u32) => out.write_char(' '),
112        // other C0 controls and DEL
113        c => write!(out, "\\x{:02x}", c as u32),
114    }
115}
116
117/// encodes `input` for safe embedding in a rust byte string literal (`b"..."`).
118///
119/// escapes backslashes, double quotes, and control characters. non-ASCII
120/// characters are encoded as their individual UTF-8 bytes using `\xHH`
121/// notation, since byte string literals only accept ASCII directly.
122///
123/// # examples
124///
125/// ```
126/// use contextual_encoder::for_rust_byte_string;
127///
128/// assert_eq!(for_rust_byte_string("hello"), "hello");
129/// assert_eq!(for_rust_byte_string(r#"say "hi""#), r#"say \"hi\""#);
130/// assert_eq!(for_rust_byte_string("café"), r"caf\xc3\xa9");
131/// assert_eq!(for_rust_byte_string("null\x00byte"), r"null\0byte");
132/// ```
133pub fn for_rust_byte_string(input: &str) -> String {
134    let mut out = String::with_capacity(input.len());
135    write_rust_byte_string(&mut out, input).expect("writing to string cannot fail");
136    out
137}
138
139/// writes the rust-byte-string-encoded form of `input` to `out`.
140///
141/// see [`for_rust_byte_string`] for encoding rules.
142pub fn write_rust_byte_string<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
143    encode_loop(out, input, needs_byte_string_encoding, |out, c, _next| {
144        write_byte_string_encoded(out, c, write_rust_named_escape)
145    })
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    // -- for_rust_string --
153
154    #[test]
155    fn string_passthrough() {
156        assert_eq!(for_rust_string("hello world"), "hello world");
157        assert_eq!(for_rust_string(""), "");
158        assert_eq!(for_rust_string("café 日本語"), "café 日本語");
159        assert_eq!(for_rust_string("😀"), "😀");
160    }
161
162    #[test]
163    fn string_escapes_double_quote() {
164        assert_eq!(for_rust_string(r#"a"b"#), r#"a\"b"#);
165    }
166
167    #[test]
168    fn string_passes_single_quote() {
169        assert_eq!(for_rust_string("a'b"), "a'b");
170    }
171
172    #[test]
173    fn string_escapes_backslash() {
174        assert_eq!(for_rust_string(r"a\b"), r"a\\b");
175    }
176
177    #[test]
178    fn string_named_escapes() {
179        assert_eq!(for_rust_string("\0"), "\\0");
180        assert_eq!(for_rust_string("\t"), "\\t");
181        assert_eq!(for_rust_string("\n"), "\\n");
182        assert_eq!(for_rust_string("\r"), "\\r");
183    }
184
185    #[test]
186    fn string_hex_escapes_for_controls() {
187        assert_eq!(for_rust_string("\x01"), "\\x01");
188        assert_eq!(for_rust_string("\x08"), "\\x08");
189        assert_eq!(for_rust_string("\x0B"), "\\x0b");
190        assert_eq!(for_rust_string("\x0C"), "\\x0c");
191        assert_eq!(for_rust_string("\x1F"), "\\x1f");
192        assert_eq!(for_rust_string("\x7F"), "\\x7f");
193    }
194
195    #[test]
196    fn string_nonchars_replaced() {
197        assert_eq!(for_rust_string("\u{FDD0}"), " ");
198        assert_eq!(for_rust_string("\u{FFFE}"), " ");
199    }
200
201    #[test]
202    fn string_writer_matches() {
203        let input = "test\0\"\\\n café";
204        let mut w = String::new();
205        write_rust_string(&mut w, input).unwrap();
206        assert_eq!(for_rust_string(input), w);
207    }
208
209    // -- for_rust_char --
210
211    #[test]
212    fn char_passthrough() {
213        assert_eq!(for_rust_char("hello world"), "hello world");
214        assert_eq!(for_rust_char(""), "");
215        assert_eq!(for_rust_char("café"), "café");
216    }
217
218    #[test]
219    fn char_escapes_single_quote() {
220        assert_eq!(for_rust_char("a'b"), r"a\'b");
221    }
222
223    #[test]
224    fn char_passes_double_quote() {
225        assert_eq!(for_rust_char(r#"a"b"#), r#"a"b"#);
226    }
227
228    #[test]
229    fn char_escapes_backslash() {
230        assert_eq!(for_rust_char(r"a\b"), r"a\\b");
231    }
232
233    #[test]
234    fn char_named_escapes() {
235        assert_eq!(for_rust_char("\0"), "\\0");
236        assert_eq!(for_rust_char("\t"), "\\t");
237        assert_eq!(for_rust_char("\n"), "\\n");
238        assert_eq!(for_rust_char("\r"), "\\r");
239    }
240
241    #[test]
242    fn char_hex_escapes_for_controls() {
243        assert_eq!(for_rust_char("\x01"), "\\x01");
244        assert_eq!(for_rust_char("\x7F"), "\\x7f");
245    }
246
247    #[test]
248    fn char_nonchars_replaced() {
249        assert_eq!(for_rust_char("\u{FDD0}"), " ");
250    }
251
252    #[test]
253    fn char_writer_matches() {
254        let input = "test\0'\\\n café";
255        let mut w = String::new();
256        write_rust_char(&mut w, input).unwrap();
257        assert_eq!(for_rust_char(input), w);
258    }
259
260    // -- for_rust_byte_string --
261
262    #[test]
263    fn byte_string_passthrough() {
264        assert_eq!(for_rust_byte_string("hello world"), "hello world");
265        assert_eq!(for_rust_byte_string(""), "");
266    }
267
268    #[test]
269    fn byte_string_escapes_double_quote() {
270        assert_eq!(for_rust_byte_string(r#"a"b"#), r#"a\"b"#);
271    }
272
273    #[test]
274    fn byte_string_escapes_backslash() {
275        assert_eq!(for_rust_byte_string(r"a\b"), r"a\\b");
276    }
277
278    #[test]
279    fn byte_string_named_escapes() {
280        assert_eq!(for_rust_byte_string("\0"), "\\0");
281        assert_eq!(for_rust_byte_string("\t"), "\\t");
282        assert_eq!(for_rust_byte_string("\n"), "\\n");
283        assert_eq!(for_rust_byte_string("\r"), "\\r");
284    }
285
286    #[test]
287    fn byte_string_hex_for_controls() {
288        assert_eq!(for_rust_byte_string("\x01"), "\\x01");
289        assert_eq!(for_rust_byte_string("\x7F"), "\\x7f");
290    }
291
292    #[test]
293    fn byte_string_non_ascii_as_utf8_bytes() {
294        // é = U+00E9 → UTF-8: C3 A9
295        assert_eq!(for_rust_byte_string("é"), r"\xc3\xa9");
296        // café → only the é is encoded
297        assert_eq!(for_rust_byte_string("café"), r"caf\xc3\xa9");
298        // 日 = U+65E5 → UTF-8: E6 97 A5
299        assert_eq!(for_rust_byte_string("日"), r"\xe6\x97\xa5");
300        // 😀 = U+1F600 → UTF-8: F0 9F 98 80
301        assert_eq!(for_rust_byte_string("😀"), r"\xf0\x9f\x98\x80");
302    }
303
304    #[test]
305    fn byte_string_nonchars_as_bytes() {
306        // U+FDD0 → UTF-8: EF B7 90
307        assert_eq!(for_rust_byte_string("\u{FDD0}"), r"\xef\xb7\x90");
308    }
309
310    #[test]
311    fn byte_string_single_quote_passes() {
312        assert_eq!(for_rust_byte_string("a'b"), "a'b");
313    }
314
315    #[test]
316    fn byte_string_writer_matches() {
317        let input = "test\0\"\\café";
318        let mut w = String::new();
319        write_rust_byte_string(&mut w, input).unwrap();
320        assert_eq!(for_rust_byte_string(input), w);
321    }
322}