uv-fs 0.0.69

This is an internal component crate of uv
Documentation
use std::io;
use std::path::Path;

#[cfg(any(target_os = "macos", target_os = "ios"))]
use std::ffi::CString;
#[cfg(any(target_os = "macos", target_os = "ios"))]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;

/// Return whether the current platform can identify individual files' physical storage.
pub const fn supports_physical_space() -> bool {
    cfg!(any(
        target_os = "linux",
        target_os = "macos",
        target_os = "ios"
    ))
}

/// Return the physical file data that would be reclaimed by deleting `path`.
///
/// The result excludes data retained by another hardlink, copy-on-write clone, or snapshot.
/// Filesystem metadata is not included.
pub fn physical_space(path: &Path, metadata: &std::fs::Metadata) -> io::Result<u64> {
    if !metadata.is_file() {
        #[cfg(unix)]
        {
            return Ok(metadata.blocks().saturating_mul(512));
        }

        #[cfg(not(unix))]
        {
            return Ok(0);
        }
    }

    #[cfg(unix)]
    if metadata.nlink() > 1 {
        return Ok(0);
    }

    #[cfg(any(target_os = "macos", target_os = "ios"))]
    {
        apple_physical_space(path)
    }

    #[cfg(target_os = "linux")]
    {
        linux_physical_space(path)
    }

    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "ios")))]
    {
        let _ = path;
        Err(io::Error::new(
            io::ErrorKind::Unsupported,
            "per-file space measurement is unsupported on this platform",
        ))
    }
}

#[cfg(any(target_os = "macos", target_os = "ios"))]
#[expect(unsafe_code)]
fn apple_physical_space(path: &Path) -> io::Result<u64> {
    let path = CString::new(path.as_os_str().as_bytes())
        .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
    let mut attributes = libc::attrlist {
        bitmapcount: libc::ATTR_BIT_MAP_COUNT,
        reserved: 0,
        commonattr: libc::ATTR_CMN_RETURNED_ATTRS,
        volattr: 0,
        dirattr: 0,
        fileattr: 0,
        forkattr: libc::ATTR_CMNEXT_PRIVATESIZE,
    };
    let mut response = [0_u8; 32];

    // SAFETY: `path` is null-terminated and valid for the duration of the call. `attributes` is a
    // valid attribute request, and `response` has enough space for the length, returned attribute
    // set, and requested private-size value.
    let result = unsafe {
        libc::getattrlist(
            path.as_ptr(),
            (&raw mut attributes).cast(),
            response.as_mut_ptr().cast(),
            response.len(),
            libc::FSOPT_ATTR_CMN_EXTENDED,
        )
    };
    if result != 0 {
        return Err(io::Error::last_os_error());
    }

    let returned_fork_attributes =
        u32::from_ne_bytes(response[20..24].try_into().map_err(io::Error::other)?);
    if returned_fork_attributes & libc::ATTR_CMNEXT_PRIVATESIZE == 0 {
        return Err(io::Error::new(
            io::ErrorKind::Unsupported,
            "the filesystem does not report private file sizes",
        ));
    }

    let private_size = i64::from_ne_bytes(response[24..32].try_into().map_err(io::Error::other)?);
    u64::try_from(private_size).map_err(io::Error::other)
}

#[cfg(target_os = "linux")]
#[expect(unsafe_code)]
fn linux_physical_space(path: &Path) -> io::Result<u64> {
    const FIEMAP_EXTENT_LAST: u32 = 0x0000_0001;
    const FIEMAP_EXTENT_UNKNOWN: u32 = 0x0000_0002;
    const FIEMAP_EXTENT_DELALLOC: u32 = 0x0000_0004;
    const FIEMAP_EXTENT_ENCODED: u32 = 0x0000_0008;
    const FIEMAP_EXTENT_NOT_ALIGNED: u32 = 0x0000_0100;
    const FIEMAP_EXTENT_DATA_INLINE: u32 = 0x0000_0200;
    const FIEMAP_EXTENT_SHARED: u32 = 0x0000_2000;
    const MAX_EXTENTS: usize = 32;

    #[derive(Default)]
    #[repr(C)]
    struct Fiemap {
        start: u64,
        length: u64,
        flags: u32,
        mapped_extents: u32,
        extent_count: u32,
        reserved: u32,
    }

    #[derive(Clone, Copy, Default)]
    #[repr(C)]
    struct FiemapExtent {
        logical: u64,
        physical: u64,
        length: u64,
        reserved64: [u64; 2],
        flags: u32,
        reserved: [u32; 3],
    }

    #[derive(Default)]
    #[repr(C)]
    struct FiemapBuffer {
        header: Fiemap,
        extents: [FiemapExtent; MAX_EXTENTS],
    }

    const FS_IOC_FIEMAP: rustix::ioctl::Opcode =
        rustix::ioctl::opcode::read_write::<Fiemap>(b'f', 11);

    let file = fs_err::File::open(path)?;
    let mut physical = 0_u64;
    let mut start = 0_u64;

    loop {
        let mut request = FiemapBuffer {
            header: Fiemap {
                start,
                length: u64::MAX.saturating_sub(start),
                extent_count: u32::try_from(MAX_EXTENTS).map_err(io::Error::other)?,
                ..Fiemap::default()
            },
            ..FiemapBuffer::default()
        };

        // SAFETY: `FS_IOC_FIEMAP` is the Linux fiemap ioctl opcode, and `request` begins with the
        // expected fiemap header followed by enough initialized storage for all requested extents.
        unsafe {
            rustix::ioctl::ioctl(
                &file,
                rustix::ioctl::Updater::<FS_IOC_FIEMAP, _>::new(&mut request),
            )?;
        }

        let mapped_extents =
            usize::try_from(request.header.mapped_extents).map_err(io::Error::other)?;
        if mapped_extents > request.extents.len() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "the filesystem returned more extents than requested",
            ));
        }
        if mapped_extents == 0 {
            return Ok(physical);
        }

        for extent in &request.extents[..mapped_extents] {
            if extent.flags
                & (FIEMAP_EXTENT_DELALLOC | FIEMAP_EXTENT_DATA_INLINE | FIEMAP_EXTENT_SHARED)
                != 0
            {
                continue;
            }

            if extent.flags
                & (FIEMAP_EXTENT_UNKNOWN | FIEMAP_EXTENT_ENCODED | FIEMAP_EXTENT_NOT_ALIGNED)
                != 0
            {
                return Err(io::Error::new(
                    io::ErrorKind::Unsupported,
                    "the filesystem cannot report the physical size of an extent",
                ));
            }

            physical = physical.saturating_add(extent.length);
        }

        let last_extent = &request.extents[mapped_extents - 1];
        if last_extent.flags & FIEMAP_EXTENT_LAST != 0 {
            return Ok(physical);
        }

        let next = last_extent.logical.saturating_add(last_extent.length);
        if next <= start {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "the filesystem returned a non-advancing extent",
            ));
        }
        start = next;
    }
}