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}