openssh/
escape.rs

1//! Escape characters that may have special meaning in a shell, including spaces.
2//! This is a modified version of the [`shell-escape::unix`] module of [`shell-escape`] crate.
3//!
4//! [`shell-escape`]: https://crates.io/crates/shell-escape
5//! [`shell-escape::unix`]: https://docs.rs/shell-escape/latest/src/shell_escape/lib.rs.html#101
6
7use std::{
8    borrow::Cow,
9    ffi::{OsStr, OsString},
10    os::unix::ffi::OsStrExt,
11    os::unix::ffi::OsStringExt,
12};
13
14fn allowed(byte: u8) -> bool {
15    matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'=' | b'/' | b',' | b'.' | b'+')
16}
17
18/// Escape characters that may have special meaning in a shell, including spaces.
19///
20/// **Note**: This function is an adaptation of [`shell-escape::unix::escape`].
21/// This function exists only for type compatibility and the implementation is
22/// almost exactly the same as [`shell-escape::unix::escape`].
23///
24/// [`shell-escape::unix::escape`]: https://docs.rs/shell-escape/latest/src/shell_escape/lib.rs.html#101
25///
26pub(crate) fn escape(s: &OsStr) -> Cow<'_, OsStr> {
27    let as_bytes = s.as_bytes();
28    let all_allowed = as_bytes.iter().copied().all(allowed);
29
30    if !as_bytes.is_empty() && all_allowed {
31        return Cow::Borrowed(s);
32    }
33
34    let mut escaped = Vec::with_capacity(as_bytes.len() + 2);
35    escaped.push(b'\'');
36
37    for &b in as_bytes {
38        match b {
39            b'\'' | b'!' => {
40                escaped.reserve(4);
41                escaped.push(b'\'');
42                escaped.push(b'\\');
43                escaped.push(b);
44                escaped.push(b'\'');
45            }
46            _ => escaped.push(b),
47        }
48    }
49    escaped.push(b'\'');
50    OsString::from_vec(escaped).into()
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    fn test_escape_case(input: &str, expected: &str) {
58        test_escape_from_bytes(input.as_bytes(), expected.as_bytes())
59    }
60
61    fn test_escape_from_bytes(input: &[u8], expected: &[u8]) {
62        let input_os_str = OsStr::from_bytes(input);
63        let observed_os_str = escape(input_os_str);
64        let expected_os_str = OsStr::from_bytes(expected);
65        assert_eq!(observed_os_str, expected_os_str);
66    }
67
68    // These tests are courtesy of the `shell-escape` crate.
69    #[test]
70    fn test_escape() {
71        test_escape_case(
72            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
73            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
74        );
75        test_escape_case("--aaa=bbb-ccc", "--aaa=bbb-ccc");
76        test_escape_case(
77            "linker=gcc -L/foo -Wl,bar",
78            r#"'linker=gcc -L/foo -Wl,bar'"#,
79        );
80        test_escape_case(r#"--features="default""#, r#"'--features="default"'"#);
81        test_escape_case(r#"'!\$`\\\n "#, r#"''\'''\!'\$`\\\n '"#);
82        test_escape_case("", r#"''"#);
83        test_escape_case(" ", r#"' '"#);
84        test_escape_case("*", r#"'*'"#);
85
86        test_escape_from_bytes(
87            &[0x66, 0x6f, 0x80, 0x6f],
88            &[b'\'', 0x66, 0x6f, 0x80, 0x6f, b'\''],
89        );
90    }
91}