1use 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
42pub 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
66pub 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
81pub 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
108pub 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
133pub 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
148fn 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 c => write!(out, "\\x{:02x}", c as u32),
163 }
164}
165
166pub 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
188pub 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 #[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 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 #[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 #[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 #[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 assert_eq!(for_rust_byte_string("é"), r"\xc3\xa9");
432 assert_eq!(for_rust_byte_string("café"), r"caf\xc3\xa9");
434 assert_eq!(for_rust_byte_string("日"), r"\xe6\x97\xa5");
436 assert_eq!(for_rust_byte_string("😀"), r"\xf0\x9f\x98\x80");
438 }
439
440 #[test]
441 fn byte_string_nonchars_as_bytes() {
442 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}