1#[cfg(windows)]
3use std::{
4 ffi::OsString,
5 os::windows::{ffi::OsStringExt, fs::MetadataExt},
6 path::PathBuf,
7};
8#[cfg(unix)]
9use std::{
10 ffi::{CStr, CString, OsStr},
11 os::{
12 fd::{AsRawFd, RawFd},
13 unix::ffi::OsStrExt,
14 },
15 ptr,
16};
17use std::{fs, io, path::Path};
18
19#[cfg(windows)]
20use windows_sys::Win32::{
21 Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE},
22 Storage::FileSystem::{
23 CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
24 FILE_FLAG_OPEN_REPARSE_POINT, FILE_LIST_DIRECTORY, FILE_SHARE_READ, FILE_SHARE_WRITE,
25 GetFinalPathNameByHandleW, OPEN_EXISTING,
26 },
27};
28
29#[cfg(unix)]
30pub fn remove_path_recursively(path: &Path) -> io::Result<()> {
31 remove_path_recursively_unix(path)
32}
33
34#[cfg(windows)]
35pub fn remove_path_recursively(path: &Path) -> io::Result<()> {
36 remove_path_recursively_windows(path)
37}
38
39#[cfg(not(any(unix, windows)))]
40pub fn remove_path_recursively(path: &Path) -> io::Result<()> {
41 let metadata = fs::symlink_metadata(path)?;
42 let file_type = metadata.file_type();
43
44 if !file_type.is_dir() {
45 return fs::remove_file(path);
46 }
47
48 for entry in fs::read_dir(path)? {
49 let entry = entry?;
50 remove_path_recursively(&entry.path())?;
51 }
52
53 fs::remove_dir(path)
54}
55
56#[cfg(windows)]
57fn remove_path_recursively_windows(path: &Path) -> io::Result<()> {
58 let metadata = fs::symlink_metadata(path)?;
59 let file_type = metadata.file_type();
60 let is_reparse_point = metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0;
61
62 if !file_type.is_dir() {
63 return fs::remove_file(path);
64 }
65
66 if is_reparse_point {
67 return fs::remove_dir(path);
68 }
69
70 let dir = open_directory_handle(path)?;
71 let stable_path = final_path_from_handle(dir.raw())?;
72
73 for entry in fs::read_dir(&stable_path)? {
74 let entry = entry?;
75 remove_path_recursively_windows(&entry.path())?;
76 }
77
78 fs::remove_dir(stable_path)
79}
80
81#[cfg(windows)]
82fn open_directory_handle(path: &Path) -> io::Result<OwnedWindowsHandle> {
83 let wide = path_to_wide(path);
84 let handle = unsafe {
85 CreateFileW(
86 wide.as_ptr(),
87 FILE_LIST_DIRECTORY,
88 FILE_SHARE_READ | FILE_SHARE_WRITE,
89 std::ptr::null(),
90 OPEN_EXISTING,
91 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
92 std::ptr::null_mut(),
93 )
94 };
95
96 if handle == INVALID_HANDLE_VALUE {
97 Err(io::Error::last_os_error())
98 } else {
99 Ok(OwnedWindowsHandle(handle))
100 }
101}
102
103#[cfg(windows)]
104fn final_path_from_handle(handle: HANDLE) -> io::Result<PathBuf> {
105 let mut buffer = vec![0u16; 32768];
106 let len =
107 unsafe { GetFinalPathNameByHandleW(handle, buffer.as_mut_ptr(), buffer.len() as u32, 0) };
108 if len == 0 {
109 return Err(io::Error::last_os_error());
110 }
111
112 let path = OsString::from_wide(&buffer[..len as usize]);
113 let stable = PathBuf::from(path);
114 Ok(stable)
115}
116
117#[cfg(windows)]
118fn path_to_wide(path: &Path) -> Vec<u16> {
119 path.as_os_str()
120 .encode_wide()
121 .chain(std::iter::once(0))
122 .collect()
123}
124
125#[cfg(windows)]
126struct OwnedWindowsHandle(HANDLE);
127
128#[cfg(windows)]
129impl OwnedWindowsHandle {
130 fn raw(&self) -> HANDLE {
131 self.0
132 }
133}
134
135#[cfg(windows)]
136impl Drop for OwnedWindowsHandle {
137 fn drop(&mut self) {
138 unsafe {
139 CloseHandle(self.0);
140 }
141 }
142}
143
144#[cfg(unix)]
145fn remove_path_recursively_unix(path: &Path) -> io::Result<()> {
146 let parent = path.parent().unwrap_or_else(|| Path::new("."));
147 let name = path.file_name().ok_or_else(|| {
148 io::Error::new(
149 io::ErrorKind::InvalidInput,
150 format!("cannot remove root path {}", path.display()),
151 )
152 })?;
153
154 let parent_dir = fs::File::open(parent)?;
155 let entry_name = cstring_from_os_str(name)?;
156 remove_path_recursively_at(parent_dir.as_raw_fd(), &entry_name)
157}
158
159#[cfg(unix)]
160fn cstring_from_os_str(path: &OsStr) -> io::Result<CString> {
161 CString::new(path.as_bytes()).map_err(|_| {
162 io::Error::new(
163 io::ErrorKind::InvalidInput,
164 format!("path contains interior NUL: {}", Path::new(path).display()),
165 )
166 })
167}
168
169#[cfg(unix)]
170fn remove_path_recursively_at(parent_fd: RawFd, entry_name: &CStr) -> io::Result<()> {
171 let metadata = stat_no_follow(parent_fd, entry_name)?;
172 let is_dir = (metadata.st_mode & libc::S_IFMT) == libc::S_IFDIR;
173
174 if !is_dir {
175 return unlink_at(parent_fd, entry_name, 0);
176 }
177
178 let child_fd = open_directory(parent_fd, entry_name)?;
179 let dir = DirHandle::from_fd(child_fd)?;
180 let dir_fd = dir.fd();
181
182 while let Some(child_name) = dir.read_entry_name()? {
183 if child_name.to_bytes() == b"." || child_name.to_bytes() == b".." {
184 continue;
185 }
186
187 remove_path_recursively_at(dir_fd, child_name)?;
188 }
189
190 drop(dir);
191 unlink_at(parent_fd, entry_name, libc::AT_REMOVEDIR)
192}
193
194#[cfg(unix)]
195fn stat_no_follow(parent_fd: RawFd, entry_name: &CStr) -> io::Result<libc::stat> {
196 let mut metadata = std::mem::MaybeUninit::<libc::stat>::uninit();
197 let rc = unsafe {
198 libc::fstatat(
199 parent_fd,
200 entry_name.as_ptr(),
201 metadata.as_mut_ptr(),
202 libc::AT_SYMLINK_NOFOLLOW,
203 )
204 };
205
206 if rc == 0 {
207 Ok(unsafe { metadata.assume_init() })
208 } else {
209 Err(io::Error::last_os_error())
210 }
211}
212
213#[cfg(unix)]
214fn open_directory(parent_fd: RawFd, entry_name: &CStr) -> io::Result<RawFd> {
215 let flags = libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY | libc::O_NOFOLLOW;
216 let fd = unsafe { libc::openat(parent_fd, entry_name.as_ptr(), flags) };
217 if fd >= 0 {
218 Ok(fd)
219 } else {
220 Err(io::Error::last_os_error())
221 }
222}
223
224#[cfg(unix)]
225fn unlink_at(parent_fd: RawFd, entry_name: &CStr, flags: libc::c_int) -> io::Result<()> {
226 let rc = unsafe { libc::unlinkat(parent_fd, entry_name.as_ptr(), flags) };
227 if rc == 0 {
228 Ok(())
229 } else {
230 Err(io::Error::last_os_error())
231 }
232}
233
234#[cfg(unix)]
235unsafe fn errno_ptr() -> *mut libc::c_int {
236 #[cfg(any(target_os = "linux", target_os = "android"))]
237 {
238 unsafe { libc::__errno_location() }
239 }
240
241 #[cfg(any(
242 target_os = "macos",
243 target_os = "ios",
244 target_os = "freebsd",
245 target_os = "dragonfly",
246 target_os = "openbsd",
247 target_os = "netbsd"
248 ))]
249 {
250 unsafe { libc::__error() }
251 }
252}
253
254#[cfg(unix)]
255struct DirHandle(*mut libc::DIR);
256
257#[cfg(unix)]
258impl DirHandle {
259 fn from_fd(fd: RawFd) -> io::Result<Self> {
260 let dir = unsafe { libc::fdopendir(fd) };
261 if dir.is_null() {
262 let err = io::Error::last_os_error();
263 unsafe {
264 libc::close(fd);
265 }
266 Err(err)
267 } else {
268 Ok(Self(dir))
269 }
270 }
271
272 fn fd(&self) -> RawFd {
273 unsafe { libc::dirfd(self.0) }
274 }
275
276 fn read_entry_name(&self) -> io::Result<Option<&CStr>> {
277 unsafe {
278 ptr::write(errno_ptr(), 0);
279 }
280
281 let entry = unsafe { libc::readdir(self.0) };
282 if entry.is_null() {
283 let err = io::Error::last_os_error();
284 if err.raw_os_error() == Some(0) {
285 Ok(None)
286 } else {
287 Err(err)
288 }
289 } else {
290 Ok(Some(unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) }))
291 }
292 }
293}
294
295#[cfg(unix)]
296impl Drop for DirHandle {
297 fn drop(&mut self) {
298 unsafe {
299 libc::closedir(self.0);
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn removes_nested_directories_without_remove_dir_all() {
310 let temp = tempfile::TempDir::new().unwrap();
311 let root = temp.path().join("tree");
312 fs::create_dir_all(root.join("nested")).unwrap();
313 fs::write(root.join("nested/file.txt"), b"hello").unwrap();
314
315 remove_path_recursively(&root).unwrap();
316
317 assert!(!root.exists());
318 }
319
320 #[cfg(unix)]
321 #[test]
322 fn removes_symlink_without_following_target() {
323 let temp = tempfile::TempDir::new().unwrap();
324 let target_dir = temp.path().join("target");
325 let link_path = temp.path().join("link");
326 fs::create_dir_all(&target_dir).unwrap();
327 fs::write(target_dir.join("file.txt"), b"keep").unwrap();
328 std::os::unix::fs::symlink(&target_dir, &link_path).unwrap();
329
330 remove_path_recursively(&link_path).unwrap();
331
332 assert!(!link_path.exists());
333 assert!(target_dir.exists());
334 assert!(target_dir.join("file.txt").exists());
335 }
336
337 #[cfg(unix)]
338 #[test]
339 fn removes_fifo_nodes() {
340 let temp = tempfile::TempDir::new().unwrap();
341 let root = temp.path().join("tree");
342 fs::create_dir_all(&root).unwrap();
343 let fifo_path = root.join("daemon.fifo");
344 let fifo_name = CString::new(fifo_path.as_os_str().as_bytes()).unwrap();
345
346 let rc = unsafe { libc::mkfifo(fifo_name.as_ptr(), 0o600) };
347 assert_eq!(
348 rc,
349 0,
350 "mkfifo should succeed: {}",
351 io::Error::last_os_error()
352 );
353
354 remove_path_recursively(&root).unwrap();
355
356 assert!(!root.exists());
357 assert!(!fifo_path.exists());
358 }
359}