Skip to main content

objects/
fs_ops.rs

1// SPDX-License-Identifier: Apache-2.0
2#[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}