Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for escaping OsStr #9

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 133 additions & 30 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Escape characters that may have special meaning in a shell.
#![doc(html_root_url="https://docs.rs/shell-escape/0.1")]
#![doc(html_root_url = "https://docs.rs/shell-escape/0.1")]

use std::borrow::Cow;
use std::env;
use std::ffi::OsStr;

/// Escape characters that may have special meaning in a shell.
pub fn escape(s: Cow<str>) -> Cow<str> {
Expand All @@ -22,6 +23,16 @@ pub fn escape(s: Cow<str>) -> Cow<str> {
}
}

/// Escape characters that may have special meaning in a shell
/// for an `OsStr`.
pub fn escape_os_str(s: &OsStr) -> Cow<'_, OsStr> {
if cfg!(unix) || env::var("MSYSTEM").is_ok() {
unix::escape_os_str(s)
} else {
unimplemented!("windows::escape_os_str")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this can land without an implementation here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think you can use std::os::windows::ffi::OsStrExt::encode_wide here to do the escaping then convert it back using std::os::windows::ffi::OsStringExt::from_wide.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NobodyXu I've added a first draft of an implementation of escape_os_str for Windows. Whenever you can find the time, I'd really appreciate a review and some assistance with a test case for test_escape_os_str_from_bytes.

}
}

/// Windows-specific escaping.
pub mod windows {
use std::borrow::Cow;
Expand All @@ -41,7 +52,7 @@ pub mod windows {
}
}
if !needs_escape {
return s
return s;
}
let mut es = String::with_capacity(s.len());
es.push('"');
Expand All @@ -67,34 +78,46 @@ pub mod windows {
break;
}
}

}
es.push('"');
es.into()
}

#[test]
fn test_escape() {
assert_eq!(escape("--aaa=bbb-ccc".into()), "--aaa=bbb-ccc");
assert_eq!(escape("linker=gcc -L/foo -Wl,bar".into()),
r#""linker=gcc -L/foo -Wl,bar""#);
assert_eq!(escape(r#"--features="default""#.into()),
r#""--features=\"default\"""#);
assert_eq!(escape(r#"\path\to\my documents\"#.into()),
r#""\path\to\my documents\\""#);
assert_eq!(escape("".into()), r#""""#);
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_escape() {
assert_eq!(escape("--aaa=bbb-ccc".into()), "--aaa=bbb-ccc");
assert_eq!(
escape("linker=gcc -L/foo -Wl,bar".into()),
r#""linker=gcc -L/foo -Wl,bar""#
);
assert_eq!(
escape(r#"--features="default""#.into()),
r#""--features=\"default\"""#
);
assert_eq!(
escape(r#"\path\to\my documents\"#.into()),
r#""\path\to\my documents\\""#
);
assert_eq!(escape("".into()), r#""""#);
}
}
}

/// Unix-specific escaping.
pub mod unix {
use std::borrow::Cow;
use std::{
borrow::Cow,
ffi::{OsStr, OsString},
os::unix::ffi::OsStrExt,
os::unix::ffi::OsStringExt,
};

fn non_whitelisted(ch: char) -> bool {
match ch {
'a'...'z' | 'A'...'Z' | '0'...'9' | '-' | '_' | '=' | '/' | ',' | '.' | '+' => false,
_ => true,
}
!matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '=' | '/' | ',' | '.' | '+')
}

/// Escape characters that may have special meaning in a shell, including spaces.
Expand All @@ -119,17 +142,97 @@ pub mod unix {
es.into()
}

#[test]
fn test_escape() {
assert_eq!(
escape("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+".into()),
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"
);
assert_eq!(escape("--aaa=bbb-ccc".into()), "--aaa=bbb-ccc");
assert_eq!(escape("linker=gcc -L/foo -Wl,bar".into()), r#"'linker=gcc -L/foo -Wl,bar'"#);
assert_eq!(escape(r#"--features="default""#.into()), r#"'--features="default"'"#);
assert_eq!(escape(r#"'!\$`\\\n "#.into()), r#"''\'''\!'\$`\\\n '"#);
assert_eq!(escape("".into()), r#"''"#);
fn allowed(byte: u8) -> bool {
matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'=' | b'/' | b',' | b'.' | b'+')
}
}

/// Escape characters that may have special meaning in a shell, including spaces.
/// Work with `OsStr` instead of `str`.
pub fn escape_os_str(s: &OsStr) -> Cow<'_, OsStr> {
let as_bytes = s.as_bytes();
let all_whitelisted = as_bytes.iter().copied().all(allowed);

if !as_bytes.is_empty() && all_whitelisted {
return Cow::Borrowed(s);
}

let mut escaped = Vec::with_capacity(as_bytes.len() + 2);
escaped.push(b'\'');

for &b in as_bytes {
match b {
b'\'' | b'!' => {
escaped.reserve(4);
escaped.push(b'\'');
escaped.push(b'\\');
escaped.push(b);
escaped.push(b'\'');
}
_ => escaped.push(b),
}
}
escaped.push(b'\'');
OsString::from_vec(escaped).into()
}
#[cfg(test)]
mod tests {
use super::{escape, escape_os_str};
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;

#[test]
fn test_escape() {
assert_eq!(
escape(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+".into()
),
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"
);
assert_eq!(escape("--aaa=bbb-ccc".into()), "--aaa=bbb-ccc");
assert_eq!(
escape("linker=gcc -L/foo -Wl,bar".into()),
r#"'linker=gcc -L/foo -Wl,bar'"#
);
assert_eq!(
escape(r#"--features="default""#.into()),
r#"'--features="default"'"#
);
assert_eq!(escape(r#"'!\$`\\\n "#.into()), r#"''\'''\!'\$`\\\n '"#);
assert_eq!(escape("".into()), r#"''"#);
}

fn test_escape_os_str_case(input: &str, expected: &str) {
test_escape_os_str_from_bytes(input.as_bytes(), expected.as_bytes())
}

fn test_escape_os_str_from_bytes(input: &[u8], expected: &[u8]) {
let input_os_str = OsStr::from_bytes(input);
let observed_os_str = escape_os_str(input_os_str);
let expected_os_str = OsStr::from_bytes(expected);
assert_eq!(observed_os_str, expected_os_str);
}

#[test]
fn test_escape_os_str() {
test_escape_os_str_case(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
);
test_escape_os_str_case("--aaa=bbb-ccc", "--aaa=bbb-ccc");
test_escape_os_str_case(
"linker=gcc -L/foo -Wl,bar",
r#"'linker=gcc -L/foo -Wl,bar'"#,
);
test_escape_os_str_case(r#"--features="default""#, r#"'--features="default"'"#);
test_escape_os_str_case(r#"'!\$`\\\n "#, r#"''\'''\!'\$`\\\n '"#);
test_escape_os_str_case("", r#"''"#);
test_escape_os_str_case(" ", r#"' '"#);

test_escape_os_str_from_bytes(
&[0x66, 0x6f, 0x80, 0x6f],
&[b'\'', 0x66, 0x6f, 0x80, 0x6f, b'\''],
);
}
}

}