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;
#[cfg(target_os = "linux")]
use linux_raw_sys::ioctl::{
FIEMAP_EXTENT_DATA_INLINE, FIEMAP_EXTENT_DELALLOC, FIEMAP_EXTENT_ENCODED, FIEMAP_EXTENT_LAST,
FIEMAP_EXTENT_NOT_ALIGNED, FIEMAP_EXTENT_SHARED, FIEMAP_EXTENT_UNKNOWN, FS_IOC_FIEMAP,
};
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "ios"))]
use rustix::io::Errno;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PhysicalSpaceError {
#[error("the filesystem does not support physical space accounting")]
UnsupportedFilesystem,
#[error(transparent)]
UnmeasurableFile(#[from] io::Error),
}
pub const fn supports_physical_space() -> bool {
cfg!(any(
target_os = "linux",
target_os = "macos",
target_os = "ios"
))
}
pub fn physical_space(
path: &Path,
metadata: &std::fs::Metadata,
) -> Result<u64, PhysicalSpaceError> {
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(PhysicalSpaceError::UnsupportedFilesystem)
}
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[expect(unsafe_code)]
fn apple_physical_space(path: &Path) -> Result<u64, PhysicalSpaceError> {
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];
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 {
let error = io::Error::last_os_error();
if Errno::from_io_error(&error) == Some(Errno::NOTSUP) {
return Err(PhysicalSpaceError::UnsupportedFilesystem);
}
return Err(error.into());
}
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(PhysicalSpaceError::UnsupportedFilesystem);
}
let private_size = i64::from_ne_bytes(response[24..32].try_into().map_err(io::Error::other)?);
Ok(u64::try_from(private_size).map_err(io::Error::other)?)
}
#[cfg(target_os = "linux")]
#[expect(unsafe_code)]
fn linux_physical_space(path: &Path) -> Result<u64, PhysicalSpaceError> {
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],
}
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()
};
unsafe {
rustix::ioctl::ioctl(
&file,
rustix::ioctl::Updater::<{ FS_IOC_FIEMAP as rustix::ioctl::Opcode }, _>::new(
&mut request,
),
)
}
.map_err(|error| match error {
Errno::NOTSUP | Errno::NOTTY => PhysicalSpaceError::UnsupportedFilesystem,
error => PhysicalSpaceError::UnmeasurableFile(error.into()),
})?;
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",
)
.into());
}
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",
)
.into());
}
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",
)
.into());
}
start = next;
}
}