1#![no_std]
37#![forbid(unsafe_code)]
38#![warn(missing_docs)]
39
40use core::fmt::{self, Display, Formatter};
41
42#[cfg(feature = "std")]
43extern crate std;
44
45#[cfg(all(feature = "alloc", not(feature = "std")))]
49extern crate alloc;
50
51#[cfg(feature = "windows")]
52#[cfg(feature = "std")]
53use std as alloc;
54
55#[cfg(feature = "native")]
56#[cfg(feature = "std")]
57use std::{ffi::OsStr, path::Path};
58
59#[cfg(any(feature = "unix", all(feature = "native", not(windows))))]
60mod unix;
61#[cfg(any(feature = "windows", all(feature = "native", windows)))]
62mod windows;
63
64#[derive(Debug, Copy, Clone)]
66pub struct Quoted<'a> {
67 source: Kind<'a>,
68 force_quote: bool,
69 #[cfg(any(feature = "windows", all(feature = "native", windows)))]
70 external: bool,
71}
72
73#[derive(Debug, Copy, Clone)]
74enum Kind<'a> {
75 #[cfg(any(feature = "unix", all(feature = "native", not(windows))))]
76 Unix(&'a str),
77 #[cfg(feature = "unix")]
78 UnixRaw(&'a [u8]),
79 #[cfg(any(feature = "windows", all(feature = "native", windows)))]
80 Windows(&'a str),
81 #[cfg(feature = "windows")]
82 #[cfg(feature = "alloc")]
83 WindowsRaw(&'a [u16]),
84 #[cfg(feature = "native")]
85 #[cfg(feature = "std")]
86 NativeRaw(&'a std::ffi::OsStr),
87}
88
89impl<'a> Quoted<'a> {
90 fn new(source: Kind<'a>) -> Self {
91 Quoted {
92 source,
93 force_quote: true,
94 #[cfg(any(feature = "windows", all(feature = "native", windows)))]
95 external: false,
96 }
97 }
98
99 #[cfg(feature = "native")]
104 pub fn native(text: &'a str) -> Self {
105 #[cfg(windows)]
106 return Quoted::new(Kind::Windows(text));
107 #[cfg(not(windows))]
108 return Quoted::new(Kind::Unix(text));
109 }
110
111 #[cfg(feature = "native")]
116 #[cfg(feature = "std")]
117 pub fn native_raw(text: &'a OsStr) -> Self {
118 Quoted::new(Kind::NativeRaw(text))
119 }
120
121 #[cfg(feature = "unix")]
126 pub fn unix(text: &'a str) -> Self {
127 Quoted::new(Kind::Unix(text))
128 }
129
130 #[cfg(feature = "unix")]
135 pub fn unix_raw(bytes: &'a [u8]) -> Self {
136 Quoted::new(Kind::UnixRaw(bytes))
137 }
138
139 #[cfg(feature = "windows")]
144 pub fn windows(text: &'a str) -> Self {
145 Quoted::new(Kind::Windows(text))
146 }
147
148 #[cfg(feature = "windows")]
153 #[cfg(feature = "alloc")]
154 pub fn windows_raw(units: &'a [u16]) -> Self {
155 Quoted::new(Kind::WindowsRaw(units))
156 }
157
158 pub fn force(mut self, force: bool) -> Self {
163 self.force_quote = force;
164 self
165 }
166
167 #[cfg(any(feature = "windows", feature = "native"))]
188 #[allow(unused_mut, unused_variables)]
189 pub fn external(mut self, external: bool) -> Self {
190 #[cfg(any(feature = "windows", windows))]
191 {
192 self.external = external;
193 }
194 self
195 }
196}
197
198impl<'a> Display for Quoted<'a> {
199 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
200 match self.source {
201 #[cfg(feature = "native")]
202 #[cfg(feature = "std")]
203 Kind::NativeRaw(text) => {
204 #[cfg(unix)]
205 use std::os::unix::ffi::OsStrExt;
206 #[cfg(target_os = "wasi")]
207 use std::os::wasi::ffi::OsStrExt;
208 #[cfg(windows)]
209 use std::os::windows::ffi::OsStrExt;
210
211 #[cfg(windows)]
212 match text.to_str() {
213 Some(text) => windows::write(f, text, self.force_quote, self.external),
214 None => {
215 windows::write_escaped(f, decode_utf16(text.encode_wide()), self.external)
216 }
217 }
218 #[cfg(any(unix, target_os = "wasi"))]
219 match text.to_str() {
220 Some(text) => unix::write(f, text, self.force_quote),
221 None => unix::write_escaped(f, text.as_bytes()),
222 }
223 #[cfg(not(any(windows, unix, target_os = "wasi")))]
224 match text.to_str() {
225 Some(text) => unix::write(f, text, self.force_quote),
226 None => write!(f, "{:?}", text),
229 }
230 }
231
232 #[cfg(any(feature = "unix", all(feature = "native", not(windows))))]
233 Kind::Unix(text) => unix::write(f, text, self.force_quote),
234
235 #[cfg(feature = "unix")]
236 Kind::UnixRaw(bytes) => match core::str::from_utf8(bytes) {
237 Ok(text) => unix::write(f, text, self.force_quote),
238 Err(_) => unix::write_escaped(f, bytes),
239 },
240
241 #[cfg(any(feature = "windows", all(feature = "native", windows)))]
242 Kind::Windows(text) => windows::write(f, text, self.force_quote, self.external),
243
244 #[cfg(feature = "windows")]
245 #[cfg(feature = "alloc")]
246 Kind::WindowsRaw(units) => match alloc::string::String::from_utf16(units) {
252 Ok(text) => windows::write(f, &text, self.force_quote, self.external),
253 Err(_) => {
254 windows::write_escaped(f, decode_utf16(units.iter().cloned()), self.external)
255 }
256 },
257 }
258 }
259}
260
261#[cfg(any(feature = "windows", all(feature = "native", feature = "std", windows)))]
262#[cfg(feature = "alloc")]
263fn decode_utf16(units: impl IntoIterator<Item = u16>) -> impl Iterator<Item = Result<char, u16>> {
264 core::char::decode_utf16(units).map(|res| res.map_err(|err| err.unpaired_surrogate()))
265}
266
267fn requires_escape(ch: char) -> bool {
271 ch.is_control() || is_separator(ch)
272}
273
274fn is_separator(ch: char) -> bool {
278 ch == '\u{2028}' || ch == '\u{2029}'
279}
280
281fn is_bidi(ch: char) -> bool {
285 match ch {
286 '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}' => true,
287 _ => false,
288 }
289}
290
291#[inline(never)]
303fn is_suspicious_bidi(text: &str) -> bool {
304 #[derive(Clone, Copy, PartialEq)]
305 enum Kind {
306 Formatting,
307 Isolate,
308 }
309 const STACK_SIZE: usize = 16;
310 let mut stack: [Option<Kind>; STACK_SIZE] = [None; STACK_SIZE];
312 let mut pos = 0;
313 for ch in text.chars() {
314 match ch {
315 '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' => {
316 if pos >= STACK_SIZE {
317 return true;
319 }
320 stack[pos] = Some(Kind::Formatting);
321 pos += 1;
322 }
323 '\u{202C}' => {
324 if pos == 0 {
325 return true;
329 }
330 pos -= 1;
331 if stack[pos] != Some(Kind::Formatting) {
332 return true;
336 }
337 }
338 '\u{2066}' | '\u{2067}' | '\u{2068}' => {
339 if pos >= STACK_SIZE {
340 return true;
341 }
342 stack[pos] = Some(Kind::Isolate);
343 pos += 1;
344 }
345 '\u{2069}' => {
346 if pos == 0 {
347 return true;
348 }
349 pos -= 1;
350 if stack[pos] != Some(Kind::Isolate) {
351 return true;
352 }
353 }
354 _ => (),
355 }
356 }
357 pos != 0
358}
359
360#[cfg(feature = "native")]
361mod native {
362 use super::*;
363
364 pub trait Quotable {
370 fn quote(&self) -> Quoted<'_>;
387
388 fn maybe_quote(&self) -> Quoted<'_> {
404 let mut quoted = self.quote();
405 quoted.force_quote = false;
406 quoted
407 }
408 }
409
410 impl Quotable for str {
411 fn quote(&self) -> Quoted<'_> {
412 Quoted::native(self)
413 }
414 }
415
416 #[cfg(feature = "std")]
417 impl Quotable for OsStr {
418 fn quote(&self) -> Quoted<'_> {
419 Quoted::native_raw(self)
420 }
421 }
422
423 #[cfg(feature = "std")]
424 impl Quotable for Path {
425 fn quote(&self) -> Quoted<'_> {
426 Quoted::native_raw(self.as_ref())
427 }
428 }
429
430 impl<'a, T: Quotable + ?Sized> From<&'a T> for Quoted<'a> {
431 fn from(val: &'a T) -> Self {
432 val.quote()
433 }
434 }
435}
436
437#[cfg(feature = "native")]
438pub use crate::native::Quotable;
439
440#[cfg(feature = "std")]
441#[cfg(test)]
442mod tests {
443 #![allow(unused)]
444
445 use super::*;
446
447 use std::string::{String, ToString};
448
449 const BOTH_ALWAYS: &[(&str, &str)] = &[
450 ("foo", "'foo'"),
451 ("foo/bar.baz", "'foo/bar.baz'"),
452 ("can't", r#""can't""#),
453 ];
454 const BOTH_MAYBE: &[(&str, &str)] = &[
455 ("foo", "foo"),
456 ("foo bar", "'foo bar'"),
457 ("$foo", "'$foo'"),
458 ("-", "-"),
459 ("a#b", "a#b"),
460 ("#ab", "'#ab'"),
461 ("a~b", "a~b"),
462 ("!", "'!'"),
463 ("}", ("'}'")),
464 ("\u{200B}", "'\u{200B}'"),
465 ("\u{200B}a", "'\u{200B}a'"),
466 ("a\u{200B}", "a\u{200B}"),
467 ("\u{2000}", "'\u{2000}'"),
468 ("\u{2800}", "'\u{2800}'"),
469 (
471 "\u{2067}\u{2066}abc\u{2069}\u{2066}def\u{2069}\u{2069}",
472 "'\u{2067}\u{2066}abc\u{2069}\u{2066}def\u{2069}\u{2069}'",
473 ),
474 ];
475
476 const UNIX_ALWAYS: &[(&str, &str)] = &[
477 ("", "''"),
478 (r#"can'"t"#, r#"'can'\''"t'"#),
479 (r#"can'$t"#, r#"'can'\''$t'"#),
480 ("foo\nb\ta\r\\\0`r", r#"$'foo\nb\ta\r\\\x00`r'"#),
481 ("foo\x02", r#"$'foo\x02'"#),
482 (r#"'$''"#, r#"\''$'\'\'"#),
483 ];
484 const UNIX_MAYBE: &[(&str, &str)] = &[
485 ("", "''"),
486 ("-x", "-x"),
487 ("a,b", "a,b"),
488 ("a\\b", "'a\\b'"),
489 ("\x02AB", "$'\\x02'$'AB'"),
490 ("\x02GH", "$'\\x02GH'"),
491 ("\t", r#"$'\t'"#),
492 ("\r", r#"$'\r'"#),
493 ("\u{85}", r#"$'\xC2\x85'"#),
494 ("\u{85}a", r#"$'\xC2\x85'$'a'"#),
495 ("\u{2028}", r#"$'\xE2\x80\xA8'"#),
496 (
498 "user\u{202E} \u{2066}// Check if admin\u{2069} \u{2066}",
499 r#"$'user\xE2\x80\xAE \xE2\x81\xA6// Check if admin\xE2\x81\xA9 \xE2\x81\xA6'"#,
500 ),
501 ];
502 const UNIX_RAW: &[(&[u8], &str)] = &[
503 (b"foo\xFF", r#"$'foo\xFF'"#),
504 (b"foo\xFFbar", r#"$'foo\xFF'$'bar'"#),
505 ];
506
507 #[cfg(feature = "unix")]
508 #[test]
509 fn unix() {
510 for &(orig, expected) in UNIX_ALWAYS.iter().chain(BOTH_ALWAYS) {
511 assert_eq!(Quoted::unix(orig).to_string(), expected);
512 }
513 for &(orig, expected) in UNIX_MAYBE.iter().chain(BOTH_MAYBE) {
514 assert_eq!(Quoted::unix(orig).force(false).to_string(), expected);
515 }
516 for &(orig, expected) in UNIX_RAW {
517 assert_eq!(Quoted::unix_raw(orig).to_string(), expected);
518 }
519 let bidi_ok = nest_bidi(16);
520 assert_eq!(
521 Quoted::unix(&bidi_ok).to_string(),
522 "'".to_string() + &bidi_ok + "'"
523 );
524 let bidi_too_deep = nest_bidi(17);
525 assert!(Quoted::unix(&bidi_too_deep).to_string().starts_with('$'));
526 }
527
528 const WINDOWS_ALWAYS: &[(&str, &str)] = &[
529 (r#"foo\bar"#, r#"'foo\bar'"#),
530 (r#"can'"t"#, r#"'can''"t'"#),
531 (r#"can'$t"#, r#"'can''$t'"#),
532 ("foo\nb\ta\r\\\0`r", r#""foo`nb`ta`r\`0``r""#),
533 ("foo\x02", r#""foo`u{02}""#),
534 (r#"'$''"#, r#"'''$'''''"#),
535 ];
536 const WINDOWS_MAYBE: &[(&str, &str)] = &[
537 ("--%", "'--%'"),
538 ("--ok", "--ok"),
539 ("—x", "'—x'"),
540 ("a,b", "'a,b'"),
541 ("a\\b", "a\\b"),
542 ("‘", r#""‘""#),
543 (r#"‘""#, r#"''‘"'"#),
544 ("„\0", r#""`„`0""#),
545 ("\t", r#""`t""#),
546 ("\r", r#""`r""#),
547 ("\u{85}", r#""`u{85}""#),
548 ("\u{2028}", r#""`u{2028}""#),
549 (
550 "user\u{202E} \u{2066}// Check if admin\u{2069} \u{2066}",
551 r#""user`u{202E} `u{2066}// Check if admin`u{2069} `u{2066}""#,
552 ),
553 ];
554 const WINDOWS_RAW: &[(&[u16], &str)] = &[(&[b'x' as u16, 0xD800], r#""x`u{D800}""#)];
555 const WINDOWS_EXTERNAL: &[(&str, &str)] = &[
556 ("", r#"'""'"#),
557 (r#"\""#, r#"'\\\"'"#),
558 (r#"\\""#, r#"'\\\\\"'"#),
559 (r#"\x\""#, r#"'\x\\\"'"#),
560 (r#"\x\"'""#, r#"'\x\\\"''\"'"#),
561 ("\n\\\"", r#""`n\\\`"""#),
562 ("\n\\\\\"", r#""`n\\\\\`"""#),
563 ("\n\\x\\\"", r#""`n\x\\\`"""#),
564 ("\n\\x\\\"'\"", r#""`n\x\\\`"'\`"""#),
565 ("-x:", "'-x:'"),
566 ("-x.x", "'-x.x'"),
567 ("--%", r#"'"--%"'"#),
568 ("--ok", "--ok"),
569 ];
570 const WINDOWS_INTERNAL: &[(&str, &str)] = &[
571 ("", "''"),
572 (r#"can'"t"#, r#"'can''"t'"#),
573 ("-x", "'-x'"),
574 ("—x", "'—x'"),
575 ("‘\"", r#"''‘"'"#),
576 ("--%", "'--%'"),
577 ("--ok", "--ok"),
578 ];
579
580 #[cfg(feature = "windows")]
581 #[test]
582 fn windows() {
583 for &(orig, expected) in WINDOWS_ALWAYS.iter().chain(BOTH_ALWAYS) {
584 assert_eq!(Quoted::windows(orig).to_string(), expected);
585 }
586 for &(orig, expected) in WINDOWS_MAYBE.iter().chain(BOTH_MAYBE) {
587 assert_eq!(Quoted::windows(orig).force(false).to_string(), expected);
588 }
589 for &(orig, expected) in WINDOWS_RAW {
590 assert_eq!(Quoted::windows_raw(orig).to_string(), expected);
591 }
592 for &(orig, expected) in WINDOWS_EXTERNAL {
593 assert_eq!(
594 Quoted::windows(orig)
595 .force(false)
596 .external(true)
597 .to_string(),
598 expected
599 );
600 }
601 for &(orig, expected) in WINDOWS_INTERNAL {
602 assert_eq!(
603 Quoted::windows(orig)
604 .force(false)
605 .external(false)
606 .to_string(),
607 expected
608 );
609 }
610 let bidi_ok = nest_bidi(16);
611 assert_eq!(
612 Quoted::windows(&bidi_ok).to_string(),
613 "'".to_string() + &bidi_ok + "'"
614 );
615 let bidi_too_deep = nest_bidi(17);
616 assert!(Quoted::windows(&bidi_too_deep).to_string().contains('`'));
617 }
618
619 #[cfg(feature = "native")]
620 #[cfg(windows)]
621 #[test]
622 fn native() {
623 use std::ffi::OsString;
624 use std::os::windows::ffi::OsStringExt;
625
626 assert_eq!("'\"".quote().to_string(), r#"'''"'"#);
627 assert_eq!("x\0".quote().to_string(), r#""x`0""#);
628 assert_eq!(
629 OsString::from_wide(&[b'x' as u16, 0xD800])
630 .quote()
631 .to_string(),
632 r#""x`u{D800}""#
633 );
634 }
635
636 #[cfg(feature = "native")]
637 #[cfg(any(unix, target_os = "wasi"))]
638 #[test]
639 fn native() {
640 #[cfg(unix)]
641 use std::os::unix::ffi::OsStrExt;
642 #[cfg(target_os = "wasi")]
643 use std::os::wasi::ffi::OsStrExt;
644
645 assert_eq!("'\"".quote().to_string(), r#"\''"'"#);
646 assert_eq!("x\0".quote().to_string(), r#"$'x\x00'"#);
647 assert_eq!(
648 OsStr::from_bytes(b"x\xFF").quote().to_string(),
649 r#"$'x\xFF'"#
650 );
651 }
652
653 #[cfg(feature = "native")]
654 #[cfg(not(any(windows, unix, target_os = "wasi")))]
655 #[test]
656 fn native() {
657 assert_eq!("'\"".quote().to_string(), r#"\''"'"#);
658 assert_eq!("x\0".quote().to_string(), r#"$'x\x00'"#);
659 }
660
661 #[cfg(feature = "native")]
662 #[test]
663 fn can_quote_types() {
664 use std::borrow::{Cow, ToOwned};
665
666 "foo".quote();
667 "foo".to_owned().quote();
668 Cow::Borrowed("foo").quote();
669
670 OsStr::new("foo").quote();
671 OsStr::new("foo").to_owned().quote();
672 Cow::Borrowed(OsStr::new("foo")).quote();
673
674 Path::new("foo").quote();
675 Path::new("foo").to_owned().quote();
676 Cow::Borrowed(Path::new("foo")).quote();
677 }
678
679 fn nest_bidi(n: usize) -> String {
680 let mut out = String::new();
681 for _ in 0..n {
682 out.push('\u{2066}');
683 }
684 out.push('a');
685 for _ in 0..n {
686 out.push('\u{2069}');
687 }
688 out
689 }
690}