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::{
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}