use std::path::Path;
pub fn private_size(path: &Path) -> Option<u64> {
imp::private_size(path)
}
#[cfg(target_os = "macos")]
mod imp {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
const ATTR_CMN_RETURNED_ATTRS: u32 = 0x8000_0000;
const ATTR_CMNEXT_PRIVATESIZE: u32 = 0x8;
const FSOPT_NOFOLLOW: u32 = 0x1;
const FSOPT_ATTR_CMN_EXTENDED: u32 = 0x20;
#[repr(C, packed(4))]
struct Reply {
length: u32,
returned: [u32; 5],
private_size: i64,
}
pub fn private_size(path: &Path) -> Option<u64> {
let c_path = CString::new(path.as_os_str().as_bytes()).ok()?;
let mut request = libc::attrlist {
bitmapcount: libc::ATTR_BIT_MAP_COUNT,
reserved: 0,
commonattr: ATTR_CMN_RETURNED_ATTRS,
volattr: 0,
dirattr: 0,
fileattr: 0,
forkattr: ATTR_CMNEXT_PRIVATESIZE,
};
let mut reply = Reply {
length: 0,
returned: [0; 5],
private_size: 0,
};
let status = unsafe {
libc::getattrlist(
c_path.as_ptr(),
(&raw mut request).cast(),
(&raw mut reply).cast(),
size_of::<Reply>(),
FSOPT_NOFOLLOW | FSOPT_ATTR_CMN_EXTENDED,
)
};
if status != 0 {
return None;
}
let length = reply.length;
let returned = reply.returned;
if (length as usize) < size_of::<Reply>() || returned[4] & ATTR_CMNEXT_PRIVATESIZE == 0 {
return None;
}
u64::try_from(reply.private_size).ok()
}
}
#[cfg(target_os = "linux")]
mod imp {
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::Path;
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct Fiemap {
start: u64,
length: u64,
flags: u32,
mapped_extents: u32,
extent_count: u32,
reserved: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct Extent {
logical: u64,
physical: u64,
length: u64,
reserved64: [u64; 2],
flags: u32,
reserved: [u32; 3],
}
const BATCH: usize = 64;
#[repr(C)]
struct Request {
head: Fiemap,
extents: [Extent; BATCH],
}
const FS_IOC_FIEMAP: libc::Ioctl = libc::_IOWR::<Fiemap>(b'f' as u32, 11);
const FIEMAP_EXTENT_LAST: u32 = 0x1;
const FIEMAP_EXTENT_SHARED: u32 = 0x2000;
pub fn private_size(path: &Path) -> Option<u64> {
let file = OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
.open(path)
.ok()?;
let mut private = 0u64;
let mut start = 0u64;
loop {
let mut request = Request {
head: Fiemap {
start,
length: u64::MAX,
extent_count: BATCH as u32,
..Fiemap::default()
},
extents: [Extent::default(); BATCH],
};
let status = unsafe { libc::ioctl(file.as_raw_fd(), FS_IOC_FIEMAP, &raw mut request) };
if status != 0 {
return None;
}
let mapped = (request.head.mapped_extents as usize).min(BATCH);
if mapped == 0 {
return Some(private);
}
for extent in &request.extents[..mapped] {
if extent.flags & FIEMAP_EXTENT_SHARED == 0 {
private = private.saturating_add(extent.length);
}
if extent.flags & FIEMAP_EXTENT_LAST != 0 {
return Some(private);
}
start = extent.logical.saturating_add(extent.length);
}
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
mod imp {
use std::path::Path;
pub fn private_size(_path: &Path) -> Option<u64> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn a_plain_file_is_all_its_own() {
let tmp = tempfile::tempdir().unwrap();
let file = tmp.path().join("plain");
fs::write(&file, vec![9u8; 1 << 20]).unwrap();
fs::File::open(&file).unwrap().sync_all().unwrap();
let Some(private) = private_size(&file) else {
return; };
assert_eq!(
private,
crate::fsutil::allocated(&fs::metadata(&file).unwrap())
);
assert_eq!(private_size(&tmp.path().join("missing")), None);
}
#[cfg(target_os = "macos")]
#[test]
fn clones_share_their_blocks() {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let tmp = tempfile::tempdir().unwrap();
let original = tmp.path().join("original");
fs::write(&original, vec![3u8; 4 << 20]).unwrap();
if private_size(&original).is_none() {
return; }
let copy = tmp.path().join("copy");
let src = CString::new(original.as_os_str().as_bytes()).unwrap();
let dst = CString::new(copy.as_os_str().as_bytes()).unwrap();
assert_eq!(unsafe { libc::clonefile(src.as_ptr(), dst.as_ptr(), 0) }, 0);
assert_eq!(private_size(&original), Some(0));
assert_eq!(private_size(©), Some(0));
let usage = crate::fsutil::tree_usage(tmp.path());
assert_eq!(usage.private, Some(0));
assert!(usage.allocated >= 8 << 20, "{usage:?}");
}
#[cfg(target_os = "macos")]
#[test]
fn a_filesystem_that_cannot_tell_gives_none() {
assert_eq!(private_size(Path::new("/dev/null")), None);
}
}