Skip to main content

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            && !s.is_empty()
26            && s.chars().all(is_shellsafe)
27        {
28            return f.write_str(s);
29        }
30        if let Ok(r) = shlex::bytes::try_quote(self.path.as_os_str().as_bytes()) {
31            let s = String::from_utf8_lossy(&r);
32            return f.write_str(&s);
33        }
34        // Should not happen really
35        Err(std::fmt::Error)
36    }
37}
38
39impl<'a> PathQuotedDisplay<'a> {
40    /// Given a path, quote it in a way that it would be parsed by a default
41    /// POSIX shell. If the path is nonempty UTF-8 with no spaces or shell meta-characters,
42    /// it will be exactly the same as the input.
43    pub fn new<P: AsRef<Path>>(path: &'a P) -> PathQuotedDisplay<'a> {
44        PathQuotedDisplay {
45            path: path.as_ref(),
46        }
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use std::ffi::OsStr;
53
54    use super::*;
55
56    #[test]
57    fn test_unquoted() {
58        for v in [
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            (" ", "' '"),
86            ("/some/path with spaces/", "'/some/path with spaces/'"),
87            ("/foo/!/bar&", "'/foo/!/bar&'"),
88            (r#"/path/"withquotes'"#, r#""/path/\"withquotes'""#),
89        ];
90        for (v, quoted) in cases {
91            let q = PathQuotedDisplay::new(&v).to_string();
92            assert_eq!(quoted, q.as_str());
93            // Also sanity check there's exactly one token
94            let token = shlex::split(&q).unwrap();
95            assert_eq!(1, token.len());
96            assert_eq!(v, token[0]);
97        }
98    }
99
100    #[test]
101    fn test_nonutf8() {
102        let p = Path::new(OsStr::from_bytes(b"/foo/somenonutf8\xEE/bar"));
103        assert!(p.to_str().is_none());
104        let q = PathQuotedDisplay::new(&p).to_string();
105        assert_eq!(q, r#"'/foo/somenonutf8�/bar'"#);
106    }
107}