cap_primitives/fs/
remove_dir.rs

1//! This defines `remove_dir`, the primary entrypoint to sandboxed file
2//! removal.
3
4use crate::fs::remove_dir_impl;
5#[cfg(racy_asserts)]
6use crate::fs::{
7    manually, map_result, remove_dir_unchecked, stat_unchecked, FollowSymlinks, Metadata,
8};
9use std::path::Path;
10use std::{fs, io};
11
12/// Perform a `rmdirat`-like operation, ensuring that the resolution of the
13/// path never escapes the directory tree rooted at `start`.
14#[cfg_attr(not(racy_asserts), allow(clippy::let_and_return))]
15#[inline]
16pub fn remove_dir(start: &fs::File, path: &Path) -> io::Result<()> {
17    #[cfg(racy_asserts)]
18    let stat_before = stat_unchecked(start, path, FollowSymlinks::No);
19
20    // Call the underlying implementation.
21    let result = remove_dir_impl(start, path);
22
23    #[cfg(racy_asserts)]
24    let stat_after = stat_unchecked(start, path, FollowSymlinks::No);
25
26    #[cfg(racy_asserts)]
27    check_remove_dir(start, path, &stat_before, &result, &stat_after);
28
29    result
30}
31
32#[cfg(racy_asserts)]
33#[allow(clippy::enum_glob_use)]
34fn check_remove_dir(
35    start: &fs::File,
36    path: &Path,
37    stat_before: &io::Result<Metadata>,
38    result: &io::Result<()>,
39    stat_after: &io::Result<Metadata>,
40) {
41    use io::ErrorKind::*;
42
43    match (
44        map_result(stat_before),
45        map_result(result),
46        map_result(stat_after),
47    ) {
48        (Ok(metadata), Ok(()), Err((NotFound, _))) => {
49            // TODO: Check that the path was inside the sandbox.
50            assert!(metadata.is_dir());
51        }
52
53        (Err((Other, _)), Ok(()), Err((NotFound, _))) => {
54            // TODO: Check that the path was inside the sandbox.
55        }
56
57        (_, Err((InvalidInput, _)), _) => {
58            // `remove_dir(".")` apparently returns `EINVAL`
59        }
60
61        (_, Err((kind, message)), _) => {
62            match map_result(&manually::canonicalize_with(
63                start,
64                path,
65                FollowSymlinks::No,
66            )) {
67                Ok(canon) => match map_result(&remove_dir_unchecked(start, &canon)) {
68                    Err((_unchecked_kind, _unchecked_message)) => {
69                        /* TODO: Check error messages.
70                        assert_eq!(
71                            kind,
72                            unchecked_kind,
73                            "unexpected error kind from remove_dir start='{:?}', \
74                             path='{}':\nstat_before={:#?}\nresult={:#?}\nstat_after={:#?}",
75                            start,
76                            path.display(),
77                            stat_before,
78                            result,
79                            stat_after
80                        );
81                        assert_eq!(message, unchecked_message);
82                        */
83                    }
84                    _ => {
85                        // TODO: Checking in the case it does end with ".".
86                        if !path.to_string_lossy().ends_with(".") {
87                            panic!(
88                                "unsandboxed remove_dir success on start={:?} path={:?}; expected \
89                                 {:?}: {}",
90                                start, path, kind, message
91                            );
92                        }
93                    }
94                },
95                Err((_canon_kind, _canon_message)) => {
96                    /* TODO: Check error messages.
97                    assert_eq!(kind, canon_kind, "'{}' vs '{}'", message, canon_message);
98                    assert_eq!(message, canon_message);
99                    */
100                }
101            }
102        }
103
104        other => panic!(
105            "inconsistent remove_dir checks: start='{:?}' path='{}':\n{:#?}",
106            start,
107            path.display(),
108            other,
109        ),
110    }
111
112    match stat_after {
113        Ok(unchecked_metadata) => match &result {
114            Ok(()) => panic!(
115                "file still exists after remove_dir start='{:?}', path='{}'",
116                start,
117                path.display()
118            ),
119            Err(e) => match e.kind() {
120                #[cfg(io_error_more)]
121                io::ErrorKind::NotADirectory => assert!(!unchecked_metadata.is_dir()),
122                #[cfg(io_error_more)]
123                io::ErrorKind::DirectoryNotEmpty => (),
124                io::ErrorKind::PermissionDenied
125                | io::ErrorKind::InvalidInput // `remove_dir(".")` apparently returns `EINVAL`
126                | io::ErrorKind::Other => (),        // directory not empty, among other things
127                _ => panic!(
128                    "unexpected error remove_dir'ing start='{:?}', path='{}': {:?}",
129                    start,
130                    path.display(),
131                    e
132                ),
133            },
134        },
135        Err(_unchecked_error) => match &result {
136            Ok(()) => (),
137            Err(result_error) => match result_error.kind() {
138                io::ErrorKind::PermissionDenied => (),
139                _ => {
140                    /* TODO: Check error messages.
141                    assert_eq!(result_error.to_string(), unchecked_error.to_string());
142                    assert_eq!(result_error.kind(), unchecked_error.kind());
143                    */
144                }
145            },
146        },
147    }
148}