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