os_display/
lib.rs

1//! Formatters for printing filenames and other strings in a terminal, with
2//! attention paid to special characters and invalid unicode.
3//!
4//! They will wrap quotes around them and add the necessary escapes to make
5//! them copy/paste-able into a shell.
6//!
7//! The [`Quotable`] trait adds `quote` and `maybe_quote` methods to string
8//! types. The [`Quoted`] type has constructors for more explicit control.
9//!
10//! # Examples
11//! ```
12//! use std::path::Path;
13//! use os_display::Quotable;
14//!
15//! let path = Path::new("foo/bar.baz");
16//!
17//! // Found file 'foo/bar.baz'
18//! println!("Found file {}", path.quote());
19//! // foo/bar.baz: Not found
20//! println!("{}: Not found", path.maybe_quote());
21//! ```
22//!
23//! If the `windows`/`unix` features are enabled:
24//!
25//! ```
26//! use os_display::Quoted;
27//!
28//! // "foo`nbar"
29//! # #[cfg(feature = "windows")]
30//! println!("{}", Quoted::windows("foo\nbar"));
31//! // $'foo\nbar'
32//! # #[cfg(feature = "unix")]
33//! println!("{}", Quoted::unix("foo\nbar"));
34//! ```
35
36#![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/// A wrapper around string types for displaying with quoting and escaping applied.
61#[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    /// Quote a string with the default style for the platform.
96    ///
97    /// On Windows this is PowerShell syntax, on all other platforms this is
98    /// bash/ksh syntax.
99    #[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    /// Quote an `OsStr` with the default style for the platform.
108    ///
109    /// On platforms other than Windows and Unix, if the encoding is
110    /// invalid, the `Debug` representation will be used.
111    #[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    /// Quote a string using bash/ksh syntax.
118    ///
119    /// # Optional
120    /// This requires the optional `unix` feature.
121    #[cfg(feature = "unix")]
122    pub fn unix(text: &'a str) -> Self {
123        Quoted::new(Kind::Unix(text))
124    }
125
126    /// Quote possibly invalid UTF-8 using bash/ksh syntax.
127    ///
128    /// # Optional
129    /// This requires the optional `unix` feature.
130    #[cfg(feature = "unix")]
131    pub fn unix_raw(bytes: &'a [u8]) -> Self {
132        Quoted::new(Kind::UnixRaw(bytes))
133    }
134
135    /// Quote a string using PowerShell syntax.
136    ///
137    /// # Optional
138    /// This requires the optional `windows` feature.
139    #[cfg(feature = "windows")]
140    pub fn windows(text: &'a str) -> Self {
141        Quoted::new(Kind::Windows(text))
142    }
143
144    /// Quote possibly invalid UTF-16 using PowerShell syntax.
145    ///
146    /// # Optional
147    /// This requires the optional `windows` feature and the (default) `alloc` feature.
148    #[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    /// Toggle forced quoting. If `true`, quotes are added even if no special
155    /// characters are present.
156    ///
157    /// Defaults to `true`.
158    pub fn force(mut self, force: bool) -> Self {
159        self.force_quote = force;
160        self
161    }
162
163    /// When quoting for PowerShell, toggle whether to use legacy quoting for external
164    /// programs.
165    ///
166    /// If enabled, double quotes (and sometimes backslashes) will be escaped so
167    /// that they can be passed to external programs in PowerShell versions before
168    /// 7.3, or with `$PSNativeCommandArgumentPassing` set to `'Legacy'`.
169    ///
170    /// If disabled, quoting will suit modern argument passing (always used for internal
171    /// commandlets and .NET functions). Strings that look like options or numbers will
172    /// be quoted.
173    ///
174    /// It is sadly impossible to quote a string such that it's suitable for both
175    /// modern and legacy argument passing.
176    ///
177    /// Defaults to `false`.
178    ///
179    /// # Optional
180    /// This requires either the `windows` or the `native` feature. It has no effect
181    /// on Unix-style quoting.
182    #[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                    // Debug is our best shot for not losing information.
220                    // But you probably can't paste it into a shell.
221                    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            // Avoiding this allocation is possible in theory, but it'd require either
240            // complicating or slowing down the common case.
241            // Perhaps we could offer a non-allocating API for known-invalid UTF-16 strings
242            // that we pass straight to write_escaped(), but it seems a bit awkward.
243            // Please open an issue if you have a need for this.
244            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
260/// Characters that may not be safe to print in a terminal.
261///
262/// This includes all the ASCII control characters.
263fn requires_escape(ch: char) -> bool {
264    ch.is_control() || is_separator(ch)
265}
266
267/// U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are currently the only
268/// in their categories. The terminals I tried don't treat them very specially,
269/// but gedit does.
270fn is_separator(ch: char) -> bool {
271    ch == '\u{2028}' || ch == '\u{2029}'
272}
273
274/// These two ranges in PropList.txt:
275/// LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE
276/// LEFT-TO-RIGHT ISOLATE..POP DIRECTIONAL ISOLATE
277fn is_bidi(ch: char) -> bool {
278    matches!(ch, '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}')
279}
280
281/// Check whether text uses bidi in a potentially problematic way.
282///
283/// See https://trojansource.codes/ and
284/// https://www.unicode.org/reports/tr9/tr9-42.html.
285///
286/// If text fails this check then it's handled by write_escaped(), which
287/// escapes these bidi control characters no matter what.
288///
289/// We can safely assume that there are no newlines (or unicode separators)
290/// in the text because those would get it sent to write_escaped() earlier.
291/// In unicode terms, this is all a single paragraph.
292#[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    // Can't use a Vec because of no_std
301    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                    // Suspicious amount of nesting.
308                    return true;
309                }
310                stack[pos] = Some(Kind::Formatting);
311                pos += 1;
312            }
313            '\u{202C}' => {
314                if pos == 0 {
315                    // Unpaired terminator.
316                    // Not necessarily dangerous, but suspicious and
317                    // could disrupt preceding text.
318                    return true;
319                }
320                pos -= 1;
321                if stack[pos] != Some(Kind::Formatting) {
322                    // Terminator doesn't match.
323                    // UAX #9 says to pop the stack until we find a match.
324                    // But we'll keep things simple and cautious.
325                    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    /// An extension trait to apply quoting to strings.
355    ///
356    /// This is implemented on [`str`], [`OsStr`] and [`Path`].
357    ///
358    /// For finer control, see the constructors on [`Quoted`].
359    pub trait Quotable {
360        /// Returns an object that implements [`Display`] for printing strings with
361        /// proper quoting and escaping for the platform.
362        ///
363        /// On Unix this corresponds to bash/ksh syntax, on Windows PowerShell syntax
364        /// is used.
365        ///
366        /// # Examples
367        ///
368        /// ```
369        /// use std::path::Path;
370        /// use os_display::Quotable;
371        ///
372        /// let path = Path::new("foo/bar.baz");
373        ///
374        /// println!("Found file {}", path.quote()); // Prints "Found file 'foo/bar.baz'"
375        /// ```
376        fn quote(&self) -> Quoted<'_>;
377
378        /// Like `quote()`, but don't actually add quotes unless necessary because of
379        /// whitespace or special characters.
380        ///
381        /// # Examples
382        ///
383        /// ```
384        /// use std::path::Path;
385        /// use os_display::Quotable;
386        ///
387        /// let foo = Path::new("foo/bar.baz");
388        /// let bar = "foo bar";
389        ///
390        /// println!("{}: Not found", foo.maybe_quote()); // Prints "foo/bar.baz: Not found"
391        /// println!("{}: Not found", bar.maybe_quote()); // Prints "'foo bar': Not found"
392        /// ```
393        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        // Odd but safe bidi
460        (
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        // Dangerous bidi
488        (
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}