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
Show file tree
Hide file tree
Changes from 16 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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ repository = "https://github.com/sfackler/shell-escape"
description = "Escape characters that may have a special meaning in a shell"

[dependencies]

[dev-dependencies]
test-case = "2.2.2"
130 changes: 9 additions & 121 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,128 +8,16 @@
// 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;
#[cfg(unix)]
Copy link
Owner

Choose a reason for hiding this comment

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

Making these modules conditionally compiled is a breaking change.

pub mod unix;

/// Escape characters that may have special meaning in a shell.
pub fn escape(s: Cow<str>) -> Cow<str> {
if cfg!(unix) || env::var("MSYSTEM").is_ok() {
unix::escape(s)
} else {
windows::escape(s)
}
}
#[cfg(unix)]
pub use unix::{escape, escape_os_str};

/// Windows-specific escaping.
pub mod windows {
use std::borrow::Cow;
use std::iter::repeat;

/// Escape for the windows cmd.exe shell.
///
/// See [here][msdn] for more information.
///
/// [msdn]: http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
pub fn escape(s: Cow<str>) -> Cow<str> {
let mut needs_escape = s.is_empty();
for ch in s.chars() {
match ch {
'"' | '\t' | '\n' | ' ' => needs_escape = true,
_ => {}
}
}
if !needs_escape {
return s
}
let mut es = String::with_capacity(s.len());
es.push('"');
let mut chars = s.chars().peekable();
loop {
let mut nslashes = 0;
while let Some(&'\\') = chars.peek() {
chars.next();
nslashes += 1;
}

match chars.next() {
Some('"') => {
es.extend(repeat('\\').take(nslashes * 2 + 1));
es.push('"');
}
Some(c) => {
es.extend(repeat('\\').take(nslashes));
es.push(c);
}
None => {
es.extend(repeat('\\').take(nslashes * 2));
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#""""#);
}
}

/// Unix-specific escaping.
pub mod unix {
use std::borrow::Cow;

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

/// Escape characters that may have special meaning in a shell, including spaces.
pub fn escape(s: Cow<str>) -> Cow<str> {
if !s.is_empty() && !s.contains(non_whitelisted) {
return s;
}

let mut es = String::with_capacity(s.len() + 2);
es.push('\'');
for ch in s.chars() {
match ch {
'\'' | '!' => {
es.push_str("'\\");
es.push(ch);
es.push('\'');
}
_ => es.push(ch),
}
}
es.push('\'');
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#"''"#);
}
}
#[cfg(windows)]
pub mod windows;

#[cfg(windows)]
pub use windows::{escape, escape_os_str};
168 changes: 168 additions & 0 deletions src/unix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//! Unix-specific escaping.
//!
use std::{
borrow::Cow,
ffi::{OsStr, OsString},
os::unix::ffi::{OsStrExt, OsStringExt},
};

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

/// Escape characters that may have special meaning in a shell, including spaces.
pub fn escape(s: Cow<str>) -> Cow<str> {
if !s.is_empty() && !s.contains(non_whitelisted) {
return s;
}

let mut es = String::with_capacity(s.len() + 2);
es.push('\'');
for ch in s.chars() {
match ch {
'\'' | '!' => {
es.push_str("'\\");
es.push(ch);
es.push('\'');
}
_ => es.push(ch),
}
}
es.push('\'');
es.into()
}

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 {
extern crate test_case;

use super::{escape, escape_os_str};
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;

#[test_case::test_case(
" ",
r#"' '"#
; "Space is escaped by wrapping it in single quotes."
)]
#[test_case::test_case(
"",
r#"''"#
; "Empty string is escaped by wrapping it in single quotes."
)]
#[test_case::test_case(
r#"'!\$`\\\n "#,
r#"''\'''\!'\$`\\\n '"#
; "Text with a mix of characters that require escaping are individually escaped as well as wrapping the whole thing in single quotes."
)]
#[test_case::test_case(
r#"--features="default""#,
r#"'--features="default"'"#
; "Text with a double quote is escaped by wrapping it all in single quotes."
)]
#[test_case::test_case(
"linker=gcc -L/foo -Wl,bar",
r#"'linker=gcc -L/foo -Wl,bar'"#
; "Text with a slash is escaped by wrapping it all in single quotes."
)]
#[test_case::test_case(
"--aaa=bbb-ccc",
"--aaa=bbb-ccc"
; "a flag built up entirely of allowed characters is not escaped."
)]
#[test_case::test_case(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"
; "all allowed characters that do not require escaping are not escaped"
)]
fn test_escape(input: &str, expected: &str) {
assert_eq!(escape(input.into()), expected);
}

#[test_case::test_case(
" ",
r#"' '"#
; "Space is escaped by wrapping it in single quotes."
)]
#[test_case::test_case(
"",
r#"''"#
; "Empty string is escaped by wrapping it in single quotes."
)]
#[test_case::test_case(
r#"'!\$`\\\n "#,
r#"''\'''\!'\$`\\\n '"#
; "Text with a mix of characters that require escaping are individually escaped as well as wrapping the whole thing in single quotes."
)]
#[test_case::test_case(
r#"--features="default""#,
r#"'--features="default"'"#
; "Text with a double quote is escaped by wrapping it all in single quotes."
)]
#[test_case::test_case(
"linker=gcc -L/foo -Wl,bar",
r#"'linker=gcc -L/foo -Wl,bar'"#
; "Text with a slash is escaped by wrapping it all in single quotes."
)]
#[test_case::test_case(
"--aaa=bbb-ccc",
"--aaa=bbb-ccc"
; "a flag built up entirely of allowed characters is not escaped."
)]
#[test_case::test_case(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"
; "all allowed characters that do not require escaping are not escaped"
)]
fn test_escape_os_str(input: &str, expected: &str) {
let input_os_str = OsStr::from_bytes(input.as_bytes());
let observed_os_str = escape_os_str(input_os_str);
let expected_os_str = OsStr::from_bytes(expected.as_bytes());
assert_eq!(observed_os_str, expected_os_str);
}

#[test_case::test_case(
&[0x66, 0x6f, 0x80, 0x6f],
&[b'\'', 0x66, 0x6f, 0x80, 0x6f, b'\'']
; "Bytes that are not valid UTF-8 are escaped by wrapping them in single quotes."
)]
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);
}
}
Loading