bootc_internal_utils/
path.rs

1use std::fmt::Display;
2use std::os::unix::ffi::OsStrExt;
3use std::path::Path;
4
5/// Helper to format a path.
6#[derive(Debug)]
7pub struct PathQuotedDisplay<'a> {
8    path: &'a Path,
9}
10
11/// A pretty conservative check for "shell safe" characters. These
12/// are basically ones which are very common in filenames or command line
13/// arguments, which are the primary use case for this. There are definitely
14/// characters such as '+' which are typically safe, but it's fine if
15/// we're overly conservative.
16///
17/// For bash for example: https://www.gnu.org/software/bash/manual/html_node/Definitions.html#index-metacharacter
18fn is_shellsafe(c: char) -> bool {
19    matches!(c, '/' | '.' | '-' | '_' | ',' | '=' | ':') || c.is_alphanumeric()
20}
21
22impl<'a> Display for PathQuotedDisplay<'a> {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        if let Some(s) = self.path.to_str() {
25            if s.chars().all(is_shellsafe) {
26                return f.write_str(s);
27            }
28        }
29        if let Ok(r) = shlex::bytes::try_quote(self.path.as_os_str().as_bytes()) {
30            let s = String::from_utf8_lossy(&r);
31            return f.write_str(&s);
32        }
33        // Should not happen really
34        return Err(std::fmt::Error);
35    }
36}
37
38impl<'a> PathQuotedDisplay<'a> {
39    /// Given a path, quote it in a way that it would be parsed by a default
40    /// POSIX shell. If the path is UTF-8 with no spaces or shell meta-characters,
41    /// it will be exactly the same as the input.
42    pub fn new<P: AsRef<Path>>(path: &'a P) -> PathQuotedDisplay<'a> {
43        PathQuotedDisplay {
44            path: path.as_ref(),
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use std::ffi::OsStr;
52
53    use super::*;
54
55    #[test]
56    fn test_unquoted() {
57        for v in [
58            "",
59            "foo",
60            "/foo/bar",
61            "/foo/bar/../baz",
62            "/foo9/bar10",
63            "--foo",
64            "--virtiofs=/foo,/bar",
65            "/foo:/bar",
66            "--label=type=unconfined_t",
67        ] {
68            assert_eq!(v, format!("{}", PathQuotedDisplay::new(&v)));
69        }
70    }
71
72    #[test]
73    fn test_bash_metachars() {
74        // https://www.gnu.org/software/bash/manual/html_node/Definitions.html#index-metacharacter
75        let bash_metachars = "|&;()<>";
76        for c in bash_metachars.chars() {
77            assert!(!is_shellsafe(c));
78        }
79    }
80
81    #[test]
82    fn test_quoted() {
83        let cases = [
84            (" ", "' '"),
85            ("/some/path with spaces/", "'/some/path with spaces/'"),
86            ("/foo/!/bar&", "'/foo/!/bar&'"),
87            (r#"/path/"withquotes'"#, r#""/path/\"withquotes'""#),
88        ];
89        for (v, quoted) in cases {
90            let q = PathQuotedDisplay::new(&v).to_string();
91            assert_eq!(quoted, q.as_str());
92            // Also sanity check there's exactly one token
93            let token = shlex::split(&q).unwrap();
94            assert_eq!(1, token.len());
95            assert_eq!(v, token[0]);
96        }
97    }
98
99    #[test]
100    fn test_nonutf8() {
101        let p = Path::new(OsStr::from_bytes(b"/foo/somenonutf8\xEE/bar"));
102        assert!(p.to_str().is_none());
103        let q = PathQuotedDisplay::new(&p).to_string();
104        assert_eq!(q, r#"'/foo/somenonutf8�/bar'"#);
105    }
106}