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_char_checked`] — as [`for_rust_char`], but rejects input that is
8//!   not exactly one character
9//! - [`for_rust_byte_string`] — safe for rust byte string literals (`b"..."`)
10//!
11//! # encoding rules
12//!
13//! all three encoders use rust's native escape syntax:
14//!
15//! - named escapes: `\0`, `\t`, `\n`, `\r`, `\\`
16//! - C0 controls and DEL without named escapes → `\xHH`
17//! - unicode non-characters → space (string/char) or `\xHH` per byte (byte string)
18//! - bidi formatting controls → `\u{HHHH}` (string/char) or `\xHH` per byte
19//!   (byte string)
20//!
21//! the encoders differ in which quote is escaped and how non-ASCII is handled:
22//!
23//! | encoder | quote escape | non-ASCII |
24//! |---------|-------------|-----------|
25//! | `for_rust_string` | `"` → `\"` | passes through |
26//! | `for_rust_char` | `'` → `\'` | passes through |
27//! | `for_rust_byte_string` | `"` → `\"` | each UTF-8 byte → `\xHH` |
28//!
29//! # char literal length
30//!
31//! a rust char literal holds exactly one unicode scalar value, so the char
32//! encoders require input of exactly one character. empty or longer input
33//! encodes to a literal body that does not compile.
34
35use std::fmt;
36
37use crate::engine::{
38    encode_loop, is_text_direction_control, is_unicode_noncharacter, needs_byte_string_encoding,
39    write_byte_string_encoded, write_rust_named_escape,
40};
41
42/// encodes `input` for safe embedding in a rust string literal (`"..."`).
43///
44/// escapes backslashes, double quotes, and control characters using rust's
45/// escape syntax. non-ASCII unicode passes through unchanged (valid in rust
46/// string literals). unicode non-characters are replaced with space, and the
47/// bidi formatting controls `rustc` rejects raw in a literal are escaped as
48/// `\u{HHHH}`.
49///
50/// # examples
51///
52/// ```
53/// use contextual_encoder::for_rust_string;
54///
55/// assert_eq!(for_rust_string(r#"say "hi""#), r#"say \"hi\""#);
56/// assert_eq!(for_rust_string("line\nbreak"), r"line\nbreak");
57/// assert_eq!(for_rust_string("café"), "café");
58/// assert_eq!(for_rust_string("a\u{202E}b"), r"a\u{202e}b");
59/// ```
60pub fn for_rust_string(input: &str) -> String {
61    let mut out = String::with_capacity(input.len());
62    write_rust_string(&mut out, input).expect("writing to string cannot fail");
63    out
64}
65
66/// writes the rust-string-encoded form of `input` to `out`.
67///
68/// see [`for_rust_string`] for encoding rules.
69pub fn write_rust_string<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
70    encode_loop(out, input, needs_rust_string_encoding, |out, c, _next| {
71        write_rust_text_encoded(out, c, '"')
72    })
73}
74
75fn needs_rust_string_encoding(c: char) -> bool {
76    matches!(c, '\x00'..='\x1F' | '\x7F' | '"' | '\\')
77        || is_unicode_noncharacter(c as u32)
78        || is_text_direction_control(c)
79}
80
81/// encodes `input` for safe embedding in a rust char literal (`'...'`).
82///
83/// `input` must be exactly one unicode scalar value; this function does not
84/// check, and any other input encodes to a literal body that does not
85/// compile. [`for_rust_char_checked`] reports that case instead.
86///
87/// escapes backslashes, single quotes, and control characters using rust's
88/// escape syntax. non-ASCII unicode passes through unchanged. unicode
89/// non-characters are replaced with space, and the bidi formatting controls
90/// `rustc` rejects raw in a literal are escaped as `\u{HHHH}`.
91///
92/// # examples
93///
94/// ```
95/// use contextual_encoder::for_rust_char;
96///
97/// assert_eq!(for_rust_char("'"), r"\'");
98/// assert_eq!(for_rust_char("\t"), r"\t");
99/// assert_eq!(for_rust_char("é"), "é");
100/// assert_eq!(for_rust_char("\u{202E}"), r"\u{202e}");
101/// ```
102pub fn for_rust_char(input: &str) -> String {
103    let mut out = String::with_capacity(input.len());
104    write_rust_char(&mut out, input).expect("writing to string cannot fail");
105    out
106}
107
108/// encodes `input` for a rust char literal (`'...'`), or returns `None` if
109/// `input` is not exactly one unicode scalar value.
110///
111/// the checked counterpart to [`for_rust_char`]; on `Some`, the encoding is
112/// identical. a grapheme cluster spelled with several scalar values, such as
113/// `"e\u{301}"`, is rejected — no char literal can hold it.
114///
115/// # examples
116///
117/// ```
118/// use contextual_encoder::for_rust_char_checked;
119///
120/// assert_eq!(for_rust_char_checked("'"), Some(r"\'".to_string()));
121/// assert_eq!(for_rust_char_checked("é"), Some("é".to_string()));
122/// assert_eq!(for_rust_char_checked("it's"), None);
123/// assert_eq!(for_rust_char_checked(""), None);
124/// ```
125pub fn for_rust_char_checked(input: &str) -> Option<String> {
126    let mut chars = input.chars();
127    match (chars.next(), chars.next()) {
128        (Some(_), None) => Some(for_rust_char(input)),
129        (None, _) | (Some(_), Some(_)) => None,
130    }
131}
132
133/// writes the rust-char-encoded form of `input` to `out`.
134///
135/// see [`for_rust_char`] for encoding rules and the one-character contract.
136pub fn write_rust_char<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
137    encode_loop(out, input, needs_rust_char_encoding, |out, c, _next| {
138        write_rust_text_encoded(out, c, '\'')
139    })
140}
141
142fn needs_rust_char_encoding(c: char) -> bool {
143    matches!(c, '\x00'..='\x1F' | '\x7F' | '\'' | '\\')
144        || is_unicode_noncharacter(c as u32)
145        || is_text_direction_control(c)
146}
147
148/// writes the encoded form of a character for rust string or char context.
149/// `quote` is the delimiter being escaped (`"` or `'`).
150fn write_rust_text_encoded<W: fmt::Write>(out: &mut W, c: char, quote: char) -> fmt::Result {
151    match c {
152        '\0' => out.write_str("\\0"),
153        '\t' => out.write_str("\\t"),
154        '\n' => out.write_str("\\n"),
155        '\r' => out.write_str("\\r"),
156        '\\' => out.write_str("\\\\"),
157        '"' if quote == '"' => out.write_str("\\\""),
158        '\'' if quote == '\'' => out.write_str("\\'"),
159        c if is_unicode_noncharacter(c as u32) => out.write_char(' '),
160        c if is_text_direction_control(c) => write!(out, "\\u{{{:04x}}}", c as u32),
161        // other C0 controls and DEL
162        c => write!(out, "\\x{:02x}", c as u32),
163    }
164}
165
166/// encodes `input` for safe embedding in a rust byte string literal (`b"..."`).
167///
168/// escapes backslashes, double quotes, and control characters. non-ASCII
169/// characters are encoded as their individual UTF-8 bytes using `\xHH`
170/// notation, since byte string literals only accept ASCII directly.
171///
172/// # examples
173///
174/// ```
175/// use contextual_encoder::for_rust_byte_string;
176///
177/// assert_eq!(for_rust_byte_string("hello"), "hello");
178/// assert_eq!(for_rust_byte_string(r#"say "hi""#), r#"say \"hi\""#);
179/// assert_eq!(for_rust_byte_string("café"), r"caf\xc3\xa9");
180/// assert_eq!(for_rust_byte_string("null\x00byte"), r"null\0byte");
181/// ```
182pub fn for_rust_byte_string(input: &str) -> String {
183    let mut out = String::with_capacity(input.len());
184    write_rust_byte_string(&mut out, input).expect("writing to string cannot fail");
185    out
186}
187
188/// writes the rust-byte-string-encoded form of `input` to `out`.
189///
190/// see [`for_rust_byte_string`] for encoding rules.
191pub fn write_rust_byte_string<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
192    encode_loop(out, input, needs_byte_string_encoding, |out, c, _next| {
193        write_byte_string_encoded(out, c, write_rust_named_escape)
194    })
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    // -- for_rust_string --
202
203    #[test]
204    fn string_passthrough() {
205        assert_eq!(for_rust_string("hello world"), "hello world");
206        assert_eq!(for_rust_string(""), "");
207        assert_eq!(for_rust_string("café 日本語"), "café 日本語");
208        assert_eq!(for_rust_string("😀"), "😀");
209    }
210
211    #[test]
212    fn string_escapes_double_quote() {
213        assert_eq!(for_rust_string(r#"a"b"#), r#"a\"b"#);
214    }
215
216    #[test]
217    fn string_passes_single_quote() {
218        assert_eq!(for_rust_string("a'b"), "a'b");
219    }
220
221    #[test]
222    fn string_escapes_backslash() {
223        assert_eq!(for_rust_string(r"a\b"), r"a\\b");
224    }
225
226    #[test]
227    fn string_named_escapes() {
228        assert_eq!(for_rust_string("\0"), "\\0");
229        assert_eq!(for_rust_string("\t"), "\\t");
230        assert_eq!(for_rust_string("\n"), "\\n");
231        assert_eq!(for_rust_string("\r"), "\\r");
232    }
233
234    #[test]
235    fn string_hex_escapes_for_controls() {
236        assert_eq!(for_rust_string("\x01"), "\\x01");
237        assert_eq!(for_rust_string("\x08"), "\\x08");
238        assert_eq!(for_rust_string("\x0B"), "\\x0b");
239        assert_eq!(for_rust_string("\x0C"), "\\x0c");
240        assert_eq!(for_rust_string("\x1F"), "\\x1f");
241        assert_eq!(for_rust_string("\x7F"), "\\x7f");
242    }
243
244    #[test]
245    fn string_nonchars_replaced() {
246        assert_eq!(for_rust_string("\u{FDD0}"), " ");
247        assert_eq!(for_rust_string("\u{FFFE}"), " ");
248    }
249
250    /// the codepoints `rustc` denies raw in a literal, with their escaped forms.
251    const TEXT_DIRECTION: [(&str, &str); 9] = [
252        ("\u{202A}", r"\u{202a}"),
253        ("\u{202B}", r"\u{202b}"),
254        ("\u{202C}", r"\u{202c}"),
255        ("\u{202D}", r"\u{202d}"),
256        ("\u{202E}", r"\u{202e}"),
257        ("\u{2066}", r"\u{2066}"),
258        ("\u{2067}", r"\u{2067}"),
259        ("\u{2068}", r"\u{2068}"),
260        ("\u{2069}", r"\u{2069}"),
261    ];
262
263    #[test]
264    fn string_escapes_text_direction_controls() {
265        for (raw, escaped) in TEXT_DIRECTION {
266            assert_eq!(for_rust_string(raw), escaped);
267            assert_eq!(for_rust_string(&format!("a{raw}b")), format!("a{escaped}b"));
268        }
269    }
270
271    #[test]
272    fn string_writer_matches() {
273        let input = "test\0\"\\\n café";
274        let mut w = String::new();
275        write_rust_string(&mut w, input).unwrap();
276        assert_eq!(for_rust_string(input), w);
277    }
278
279    // -- for_rust_char --
280
281    #[test]
282    fn char_passthrough() {
283        assert_eq!(for_rust_char("hello world"), "hello world");
284        assert_eq!(for_rust_char(""), "");
285        assert_eq!(for_rust_char("café"), "café");
286    }
287
288    #[test]
289    fn char_escapes_single_quote() {
290        assert_eq!(for_rust_char("a'b"), r"a\'b");
291    }
292
293    #[test]
294    fn char_passes_double_quote() {
295        assert_eq!(for_rust_char(r#"a"b"#), r#"a"b"#);
296    }
297
298    #[test]
299    fn char_escapes_backslash() {
300        assert_eq!(for_rust_char(r"a\b"), r"a\\b");
301    }
302
303    #[test]
304    fn char_named_escapes() {
305        assert_eq!(for_rust_char("\0"), "\\0");
306        assert_eq!(for_rust_char("\t"), "\\t");
307        assert_eq!(for_rust_char("\n"), "\\n");
308        assert_eq!(for_rust_char("\r"), "\\r");
309    }
310
311    #[test]
312    fn char_hex_escapes_for_controls() {
313        assert_eq!(for_rust_char("\x01"), "\\x01");
314        assert_eq!(for_rust_char("\x7F"), "\\x7f");
315    }
316
317    #[test]
318    fn char_nonchars_replaced() {
319        assert_eq!(for_rust_char("\u{FDD0}"), " ");
320    }
321
322    #[test]
323    fn char_escapes_text_direction_controls() {
324        for (raw, escaped) in TEXT_DIRECTION {
325            assert_eq!(for_rust_char(raw), escaped);
326        }
327    }
328
329    #[test]
330    fn char_writer_matches() {
331        let input = "test\0'\\\n café";
332        let mut w = String::new();
333        write_rust_char(&mut w, input).unwrap();
334        assert_eq!(for_rust_char(input), w);
335    }
336
337    // -- for_rust_char_checked --
338
339    #[test]
340    fn char_checked_rejects_empty() {
341        assert_eq!(for_rust_char_checked(""), None);
342    }
343
344    #[test]
345    fn char_checked_accepts_single() {
346        assert_eq!(for_rust_char_checked("a"), Some("a".to_string()));
347        assert_eq!(for_rust_char_checked(" "), Some(" ".to_string()));
348    }
349
350    #[test]
351    fn char_checked_rejects_multi() {
352        assert_eq!(for_rust_char_checked("ab"), None);
353        assert_eq!(for_rust_char_checked("it's"), None);
354        assert_eq!(for_rust_char_checked("hello world"), None);
355    }
356
357    #[test]
358    fn char_checked_rejects_multi_scalar_grapheme() {
359        assert_eq!(for_rust_char_checked("e\u{301}"), None);
360    }
361
362    #[test]
363    fn char_checked_escapes_single() {
364        assert_eq!(for_rust_char_checked("'"), Some(r"\'".to_string()));
365        assert_eq!(for_rust_char_checked("\\"), Some(r"\\".to_string()));
366        assert_eq!(for_rust_char_checked("\0"), Some(r"\0".to_string()));
367        assert_eq!(for_rust_char_checked("\n"), Some(r"\n".to_string()));
368        assert_eq!(for_rust_char_checked("\x01"), Some(r"\x01".to_string()));
369        assert_eq!(for_rust_char_checked("\u{FDD0}"), Some(" ".to_string()));
370    }
371
372    #[test]
373    fn char_checked_accepts_non_ascii() {
374        assert_eq!(for_rust_char_checked("é"), Some("é".to_string()));
375        assert_eq!(for_rust_char_checked("日"), Some("日".to_string()));
376        assert_eq!(for_rust_char_checked("😀"), Some("😀".to_string()));
377    }
378
379    #[test]
380    fn char_checked_escapes_text_direction_controls() {
381        for (raw, escaped) in TEXT_DIRECTION {
382            assert_eq!(for_rust_char_checked(raw), Some(escaped.to_string()));
383        }
384    }
385
386    #[test]
387    fn char_checked_matches_unchecked_when_accepted() {
388        for input in [
389            "a", "'", "\\", "\0", "\t", "\n", "\r", "\x01", "\x7F", "é", "😀", "\u{202E}",
390            "\u{2069}",
391        ] {
392            assert_eq!(for_rust_char_checked(input), Some(for_rust_char(input)));
393        }
394    }
395
396    // -- for_rust_byte_string --
397
398    #[test]
399    fn byte_string_passthrough() {
400        assert_eq!(for_rust_byte_string("hello world"), "hello world");
401        assert_eq!(for_rust_byte_string(""), "");
402    }
403
404    #[test]
405    fn byte_string_escapes_double_quote() {
406        assert_eq!(for_rust_byte_string(r#"a"b"#), r#"a\"b"#);
407    }
408
409    #[test]
410    fn byte_string_escapes_backslash() {
411        assert_eq!(for_rust_byte_string(r"a\b"), r"a\\b");
412    }
413
414    #[test]
415    fn byte_string_named_escapes() {
416        assert_eq!(for_rust_byte_string("\0"), "\\0");
417        assert_eq!(for_rust_byte_string("\t"), "\\t");
418        assert_eq!(for_rust_byte_string("\n"), "\\n");
419        assert_eq!(for_rust_byte_string("\r"), "\\r");
420    }
421
422    #[test]
423    fn byte_string_hex_for_controls() {
424        assert_eq!(for_rust_byte_string("\x01"), "\\x01");
425        assert_eq!(for_rust_byte_string("\x7F"), "\\x7f");
426    }
427
428    #[test]
429    fn byte_string_non_ascii_as_utf8_bytes() {
430        // é = U+00E9 → UTF-8: C3 A9
431        assert_eq!(for_rust_byte_string("é"), r"\xc3\xa9");
432        // café → only the é is encoded
433        assert_eq!(for_rust_byte_string("café"), r"caf\xc3\xa9");
434        // 日 = U+65E5 → UTF-8: E6 97 A5
435        assert_eq!(for_rust_byte_string("日"), r"\xe6\x97\xa5");
436        // 😀 = U+1F600 → UTF-8: F0 9F 98 80
437        assert_eq!(for_rust_byte_string("😀"), r"\xf0\x9f\x98\x80");
438    }
439
440    #[test]
441    fn byte_string_nonchars_as_bytes() {
442        // U+FDD0 → UTF-8: EF B7 90
443        assert_eq!(for_rust_byte_string("\u{FDD0}"), r"\xef\xb7\x90");
444    }
445
446    #[test]
447    fn byte_string_text_direction_controls_as_bytes() {
448        assert_eq!(for_rust_byte_string("\u{202E}"), r"\xe2\x80\xae");
449        assert_eq!(for_rust_byte_string("\u{2069}"), r"\xe2\x81\xa9");
450    }
451
452    #[test]
453    fn byte_string_single_quote_passes() {
454        assert_eq!(for_rust_byte_string("a'b"), "a'b");
455    }
456
457    #[test]
458    fn byte_string_writer_matches() {
459        let input = "test\0\"\\café";
460        let mut w = String::new();
461        write_rust_byte_string(&mut w, input).unwrap();
462        assert_eq!(for_rust_byte_string(input), w);
463    }
464}