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