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;
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) -> 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];
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()
};
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;
}
}