use std::fmt::Display;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
#[derive(Debug)]
pub struct PathQuotedDisplay<'a> {
path: &'a Path,
}
fn is_shellsafe(c: char) -> bool {
matches!(c, '/' | '.' | '-' | '_' | ',' | '=' | ':') || c.is_alphanumeric()
}
impl<'a> Display for PathQuotedDisplay<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(s) = self.path.to_str() {
if s.chars().all(is_shellsafe) {
return f.write_str(s);
}
}
if let Ok(r) = shlex::bytes::try_quote(self.path.as_os_str().as_bytes()) {
let s = String::from_utf8_lossy(&r);
return f.write_str(&s);
}
Err(std::fmt::Error)
}
}
impl<'a> PathQuotedDisplay<'a> {
pub fn new<P: AsRef<Path>>(path: &'a P) -> PathQuotedDisplay<'a> {
PathQuotedDisplay {
path: path.as_ref(),
}
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use super::*;
#[test]
fn test_unquoted() {
for v in [
"",
"foo",
"/foo/bar",
"/foo/bar/../baz",
"/foo9/bar10",
"--foo",
"--virtiofs=/foo,/bar",
"/foo:/bar",
"--label=type=unconfined_t",
] {
assert_eq!(v, format!("{}", PathQuotedDisplay::new(&v)));
}
}
#[test]
fn test_bash_metachars() {
let bash_metachars = "|&;()<>";
for c in bash_metachars.chars() {
assert!(!is_shellsafe(c));
}
}
#[test]
fn test_quoted() {
let cases = [
(" ", "' '"),
("/some/path with spaces/", "'/some/path with spaces/'"),
("/foo/!/bar&", "'/foo/!/bar&'"),
(r#"/path/"withquotes'"#, r#""/path/\"withquotes'""#),
];
for (v, quoted) in cases {
let q = PathQuotedDisplay::new(&v).to_string();
assert_eq!(quoted, q.as_str());
let token = shlex::split(&q).unwrap();
assert_eq!(1, token.len());
assert_eq!(v, token[0]);
}
}
#[test]
fn test_nonutf8() {
let p = Path::new(OsStr::from_bytes(b"/foo/somenonutf8\xEE/bar"));
assert!(p.to_str().is_none());
let q = PathQuotedDisplay::new(&p).to_string();
assert_eq!(q, r#"'/foo/somenonutf8�/bar'"#);
}
}