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(feature = "std")]
43extern crate std;
44
45// alloc was unstable in 1.31, so do some shuffling to avoid it unless necessary.
46// 1.31 works with no features and with all features.
47// 1.36 is the minimum version that supports alloc without std.
48#[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/// A wrapper around string types for displaying with quoting and escaping applied.
65#[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    /// Quote a string with the default style for the platform.
100    ///
101    /// On Windows this is PowerShell syntax, on all other platforms this is
102    /// bash/ksh syntax.
103    #[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    /// Quote an `OsStr` with the default style for the platform.
112    ///
113    /// On platforms other than Windows, Unix and WASI, if the encoding is
114    /// invalid, the `Debug` representation will be used.
115    #[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    /// Quote a string using bash/ksh syntax.
122    ///
123    /// # Optional
124    /// This requires the optional `unix` feature.
125    #[cfg(feature = "unix")]
126    pub fn unix(text: &'a str) -> Self {
127        Quoted::new(Kind::Unix(text))
128    }
129
130    /// Quote possibly invalid UTF-8 using bash/ksh syntax.
131    ///
132    /// # Optional
133    /// This requires the optional `unix` feature.
134    #[cfg(feature = "unix")]
135    pub fn unix_raw(bytes: &'a [u8]) -> Self {
136        Quoted::new(Kind::UnixRaw(bytes))
137    }
138
139    /// Quote a string using PowerShell syntax.
140    ///
141    /// # Optional
142    /// This requires the optional `windows` feature.
143    #[cfg(feature = "windows")]
144    pub fn windows(text: &'a str) -> Self {
145        Quoted::new(Kind::Windows(text))
146    }
147
148    /// Quote possibly invalid UTF-16 using PowerShell syntax.
149    ///
150    /// # Optional
151    /// This requires the optional `windows` feature and the (default) `alloc` feature.
152    #[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    /// Toggle forced quoting. If `true`, quotes are added even if no special
159    /// characters are present.
160    ///
161    /// Defaults to `true`.
162    pub fn force(mut self, force: bool) -> Self {
163        self.force_quote = force;
164        self
165    }
166
167    /// When quoting for PowerShell, toggle whether to quote for external programs.
168    ///
169    /// If enabled, double quotes (and sometimes backslashes) will be escaped so
170    /// that they can be passed to external programs.
171    ///
172    /// If disabled, quoting will suit internal commandlets and .NET functions.
173    /// Strings that look like options or numbers will be quoted.
174    ///
175    /// It is sadly impossible to quote a string such that it's suitable for both
176    /// external and internal commands.
177    ///
178    /// The experimental `PSNativeCommandArgumentPassing` feature in PowerShell 7.2
179    /// disables the stripping of double quotes and backslashes. If it's enabled
180    /// then this setting should be disabled.
181    ///
182    /// Defaults to `false`. This could change in a future (breaking) release.
183    ///
184    /// # Optional
185    /// This requires either the `windows` or the `native` feature. It has no effect
186    /// on Unix-style quoting.
187    #[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                    // Debug is our best shot for not losing information.
227                    // But you probably can't paste it into a shell.
228                    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            // Avoiding this allocation is possible in theory, but it'd require either
247            // complicating or slowing down the common case.
248            // Perhaps we could offer a non-allocating API for known-invalid UTF-16 strings
249            // that we pass straight to write_escaped(), but it seems a bit awkward.
250            // Please open an issue if you have a need for this.
251            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
267/// Characters that may not be safe to print in a terminal.
268///
269/// This includes all the ASCII control characters.
270fn requires_escape(ch: char) -> bool {
271    ch.is_control() || is_separator(ch)
272}
273
274/// U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are currently the only
275/// in their categories. The terminals I tried don't treat them very specially,
276/// but gedit does.
277fn is_separator(ch: char) -> bool {
278    ch == '\u{2028}' || ch == '\u{2029}'
279}
280
281/// These two ranges in PropList.txt:
282/// LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE
283/// LEFT-TO-RIGHT ISOLATE..POP DIRECTIONAL ISOLATE
284fn is_bidi(ch: char) -> bool {
285    match ch {
286        '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}' => true,
287        _ => false,
288    }
289}
290
291/// Check whether text uses bidi in a potentially problematic way.
292///
293/// See https://trojansource.codes/ and
294/// https://www.unicode.org/reports/tr9/tr9-42.html.
295///
296/// If text fails this check then it's handled by write_escaped(), which
297/// escapes these bidi control characters no matter what.
298///
299/// We can safely assume that there are no newlines (or unicode separators)
300/// in the text because those would get it sent to write_escaped() earlier.
301/// In unicode terms, this is all a single paragraph.
302#[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    // Can't use a Vec because of no_std
311    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                    // Suspicious amount of nesting.
318                    return true;
319                }
320                stack[pos] = Some(Kind::Formatting);
321                pos += 1;
322            }
323            '\u{202C}' => {
324                if pos == 0 {
325                    // Unpaired terminator.
326                    // Not necessarily dangerous, but suspicious and
327                    // could disrupt preceding text.
328                    return true;
329                }
330                pos -= 1;
331                if stack[pos] != Some(Kind::Formatting) {
332                    // Terminator doesn't match.
333                    // UAX #9 says to pop the stack until we find a match.
334                    // But we'll keep things simple and cautious.
335                    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    /// An extension trait to apply quoting to strings.
365    ///
366    /// This is implemented on [`str`], [`OsStr`] and [`Path`].
367    ///
368    /// For finer control, see the constructors on [`Quoted`].
369    pub trait Quotable {
370        /// Returns an object that implements [`Display`] for printing strings with
371        /// proper quoting and escaping for the platform.
372        ///
373        /// On Unix this corresponds to bash/ksh syntax, on Windows PowerShell syntax
374        /// is used.
375        ///
376        /// # Examples
377        ///
378        /// ```
379        /// use std::path::Path;
380        /// use os_display::Quotable;
381        ///
382        /// let path = Path::new("foo/bar.baz");
383        ///
384        /// println!("Found file {}", path.quote()); // Prints "Found file 'foo/bar.baz'"
385        /// ```
386        fn quote(&self) -> Quoted<'_>;
387
388        /// Like `quote()`, but don't actually add quotes unless necessary because of
389        /// whitespace or special characters.
390        ///
391        /// # Examples
392        ///
393        /// ```
394        /// use std::path::Path;
395        /// use os_display::Quotable;
396        ///
397        /// let foo = Path::new("foo/bar.baz");
398        /// let bar = "foo bar";
399        ///
400        /// println!("{}: Not found", foo.maybe_quote()); // Prints "foo/bar.baz: Not found"
401        /// println!("{}: Not found", bar.maybe_quote()); // Prints "'foo bar': Not found"
402        /// ```
403        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        // Odd but safe bidi
470        (
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        // Dangerous bidi
497        (
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}