heddle-objects 0.2.4

An AI-native version control system
Documentation
// SPDX-License-Identifier: Apache-2.0
#[cfg(windows)]
use std::{
    ffi::OsString,
    os::windows::{ffi::OsStringExt, fs::MetadataExt},
    path::PathBuf,
};
#[cfg(unix)]
use std::{
    ffi::{CStr, CString, OsStr},
    os::{
        fd::{AsRawFd, RawFd},
        unix::ffi::OsStrExt,
    },
    ptr,
};
use std::{fs, io, path::Path};

#[cfg(windows)]
use windows_sys::Win32::{
    Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE},
    Storage::FileSystem::{
        CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
        FILE_FLAG_OPEN_REPARSE_POINT, FILE_LIST_DIRECTORY, FILE_SHARE_READ, FILE_SHARE_WRITE,
        GetFinalPathNameByHandleW, OPEN_EXISTING,
    },
};

#[cfg(unix)]
pub fn remove_path_recursively(path: &Path) -> io::Result<()> {
    remove_path_recursively_unix(path)
}

#[cfg(windows)]
pub fn remove_path_recursively(path: &Path) -> io::Result<()> {
    remove_path_recursively_windows(path)
}

#[cfg(not(any(unix, windows)))]
pub fn remove_path_recursively(path: &Path) -> io::Result<()> {
    let metadata = fs::symlink_metadata(path)?;
    let file_type = metadata.file_type();

    if !file_type.is_dir() {
        return fs::remove_file(path);
    }

    for entry in fs::read_dir(path)? {
        let entry = entry?;
        remove_path_recursively(&entry.path())?;
    }

    fs::remove_dir(path)
}

#[cfg(windows)]
fn remove_path_recursively_windows(path: &Path) -> io::Result<()> {
    let metadata = fs::symlink_metadata(path)?;
    let file_type = metadata.file_type();
    let is_reparse_point = metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0;

    if !file_type.is_dir() {
        return fs::remove_file(path);
    }

    if is_reparse_point {
        return fs::remove_dir(path);
    }

    let dir = open_directory_handle(path)?;
    let stable_path = final_path_from_handle(dir.raw())?;

    for entry in fs::read_dir(&stable_path)? {
        let entry = entry?;
        remove_path_recursively_windows(&entry.path())?;
    }

    fs::remove_dir(stable_path)
}

#[cfg(windows)]
fn open_directory_handle(path: &Path) -> io::Result<OwnedWindowsHandle> {
    let wide = path_to_wide(path);
    let handle = unsafe {
        CreateFileW(
            wide.as_ptr(),
            FILE_LIST_DIRECTORY,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            std::ptr::null(),
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
            std::ptr::null_mut(),
        )
    };

    if handle == INVALID_HANDLE_VALUE {
        Err(io::Error::last_os_error())
    } else {
        Ok(OwnedWindowsHandle(handle))
    }
}

#[cfg(windows)]
fn final_path_from_handle(handle: HANDLE) -> io::Result<PathBuf> {
    let mut buffer = vec![0u16; 32768];
    let len =
        unsafe { GetFinalPathNameByHandleW(handle, buffer.as_mut_ptr(), buffer.len() as u32, 0) };
    if len == 0 {
        return Err(io::Error::last_os_error());
    }

    let path = OsString::from_wide(&buffer[..len as usize]);
    let stable = PathBuf::from(path);
    Ok(stable)
}

#[cfg(windows)]
fn path_to_wide(path: &Path) -> Vec<u16> {
    path.as_os_str()
        .encode_wide()
        .chain(std::iter::once(0))
        .collect()
}

#[cfg(windows)]
struct OwnedWindowsHandle(HANDLE);

#[cfg(windows)]
impl OwnedWindowsHandle {
    fn raw(&self) -> HANDLE {
        self.0
    }
}

#[cfg(windows)]
impl Drop for OwnedWindowsHandle {
    fn drop(&mut self) {
        unsafe {
            CloseHandle(self.0);
        }
    }
}

#[cfg(unix)]
fn remove_path_recursively_unix(path: &Path) -> io::Result<()> {
    let parent = path.parent().unwrap_or_else(|| Path::new("."));
    let name = path.file_name().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("cannot remove root path {}", path.display()),
        )
    })?;

    let parent_dir = fs::File::open(parent)?;
    let entry_name = cstring_from_os_str(name)?;
    remove_path_recursively_at(parent_dir.as_raw_fd(), &entry_name)
}

#[cfg(unix)]
fn cstring_from_os_str(path: &OsStr) -> io::Result<CString> {
    CString::new(path.as_bytes()).map_err(|_| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("path contains interior NUL: {}", Path::new(path).display()),
        )
    })
}

#[cfg(unix)]
fn remove_path_recursively_at(parent_fd: RawFd, entry_name: &CStr) -> io::Result<()> {
    let metadata = stat_no_follow(parent_fd, entry_name)?;
    let is_dir = (metadata.st_mode & libc::S_IFMT) == libc::S_IFDIR;

    if !is_dir {
        return unlink_at(parent_fd, entry_name, 0);
    }

    let child_fd = open_directory(parent_fd, entry_name)?;
    let dir = DirHandle::from_fd(child_fd)?;
    let dir_fd = dir.fd();

    while let Some(child_name) = dir.read_entry_name()? {
        if child_name.to_bytes() == b"." || child_name.to_bytes() == b".." {
            continue;
        }

        remove_path_recursively_at(dir_fd, child_name)?;
    }

    drop(dir);
    unlink_at(parent_fd, entry_name, libc::AT_REMOVEDIR)
}

#[cfg(unix)]
fn stat_no_follow(parent_fd: RawFd, entry_name: &CStr) -> io::Result<libc::stat> {
    let mut metadata = std::mem::MaybeUninit::<libc::stat>::uninit();
    let rc = unsafe {
        libc::fstatat(
            parent_fd,
            entry_name.as_ptr(),
            metadata.as_mut_ptr(),
            libc::AT_SYMLINK_NOFOLLOW,
        )
    };

    if rc == 0 {
        Ok(unsafe { metadata.assume_init() })
    } else {
        Err(io::Error::last_os_error())
    }
}

#[cfg(unix)]
fn open_directory(parent_fd: RawFd, entry_name: &CStr) -> io::Result<RawFd> {
    let flags = libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY | libc::O_NOFOLLOW;
    let fd = unsafe { libc::openat(parent_fd, entry_name.as_ptr(), flags) };
    if fd >= 0 {
        Ok(fd)
    } else {
        Err(io::Error::last_os_error())
    }
}

#[cfg(unix)]
fn unlink_at(parent_fd: RawFd, entry_name: &CStr, flags: libc::c_int) -> io::Result<()> {
    let rc = unsafe { libc::unlinkat(parent_fd, entry_name.as_ptr(), flags) };
    if rc == 0 {
        Ok(())
    } else {
        Err(io::Error::last_os_error())
    }
}

#[cfg(unix)]
unsafe fn errno_ptr() -> *mut libc::c_int {
    #[cfg(any(target_os = "linux", target_os = "android"))]
    {
        unsafe { libc::__errno_location() }
    }

    #[cfg(any(
        target_os = "macos",
        target_os = "ios",
        target_os = "freebsd",
        target_os = "dragonfly",
        target_os = "openbsd",
        target_os = "netbsd"
    ))]
    {
        unsafe { libc::__error() }
    }
}

#[cfg(unix)]
struct DirHandle(*mut libc::DIR);

#[cfg(unix)]
impl DirHandle {
    fn from_fd(fd: RawFd) -> io::Result<Self> {
        let dir = unsafe { libc::fdopendir(fd) };
        if dir.is_null() {
            let err = io::Error::last_os_error();
            unsafe {
                libc::close(fd);
            }
            Err(err)
        } else {
            Ok(Self(dir))
        }
    }

    fn fd(&self) -> RawFd {
        unsafe { libc::dirfd(self.0) }
    }

    fn read_entry_name(&self) -> io::Result<Option<&CStr>> {
        unsafe {
            ptr::write(errno_ptr(), 0);
        }

        let entry = unsafe { libc::readdir(self.0) };
        if entry.is_null() {
            let err = io::Error::last_os_error();
            if err.raw_os_error() == Some(0) {
                Ok(None)
            } else {
                Err(err)
            }
        } else {
            Ok(Some(unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) }))
        }
    }
}

#[cfg(unix)]
impl Drop for DirHandle {
    fn drop(&mut self) {
        unsafe {
            libc::closedir(self.0);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn removes_nested_directories_without_remove_dir_all() {
        let temp = tempfile::TempDir::new().unwrap();
        let root = temp.path().join("tree");
        fs::create_dir_all(root.join("nested")).unwrap();
        fs::write(root.join("nested/file.txt"), b"hello").unwrap();

        remove_path_recursively(&root).unwrap();

        assert!(!root.exists());
    }

    #[cfg(unix)]
    #[test]
    fn removes_symlink_without_following_target() {
        let temp = tempfile::TempDir::new().unwrap();
        let target_dir = temp.path().join("target");
        let link_path = temp.path().join("link");
        fs::create_dir_all(&target_dir).unwrap();
        fs::write(target_dir.join("file.txt"), b"keep").unwrap();
        std::os::unix::fs::symlink(&target_dir, &link_path).unwrap();

        remove_path_recursively(&link_path).unwrap();

        assert!(!link_path.exists());
        assert!(target_dir.exists());
        assert!(target_dir.join("file.txt").exists());
    }

    #[cfg(unix)]
    #[test]
    fn removes_fifo_nodes() {
        let temp = tempfile::TempDir::new().unwrap();
        let root = temp.path().join("tree");
        fs::create_dir_all(&root).unwrap();
        let fifo_path = root.join("daemon.fifo");
        let fifo_name = CString::new(fifo_path.as_os_str().as_bytes()).unwrap();

        let rc = unsafe { libc::mkfifo(fifo_name.as_ptr(), 0o600) };
        assert_eq!(
            rc,
            0,
            "mkfifo should succeed: {}",
            io::Error::last_os_error()
        );

        remove_path_recursively(&root).unwrap();

        assert!(!root.exists());
        assert!(!fifo_path.exists());
    }
}