use std::os::unix::io::RawFd;
pub const PUNCH_HOLE_ALIGNMENT: u64 = 4096;
fn block_aligned_interior(start: u64, end: u64) -> Option<(u64, u64)> {
let aligned_start =
start.saturating_add(PUNCH_HOLE_ALIGNMENT - 1) & !(PUNCH_HOLE_ALIGNMENT - 1);
let aligned_end = end & !(PUNCH_HOLE_ALIGNMENT - 1);
(aligned_end > aligned_start).then_some((aligned_start, aligned_end))
}
#[must_use]
pub fn aligned_punch_range(start: u64, end: u64) -> Option<(u64, u64)> {
block_aligned_interior(start, end).map(|(s, e)| (s, e - s))
}
pub fn zero_range(fd: RawFd, start: u64, end: u64) -> std::io::Result<()> {
if end <= start {
return Ok(());
}
match block_aligned_interior(start, end) {
Some((aligned_start, aligned_end)) => {
if punch_hole(fd, aligned_start, aligned_end - aligned_start).is_err() {
zero_pwrite(fd, aligned_start, aligned_end)?;
}
zero_pwrite(fd, start, aligned_start)?; zero_pwrite(fd, aligned_end, end)?; }
None => zero_pwrite(fd, start, end)?,
}
Ok(())
}
fn zero_pwrite(fd: RawFd, start: u64, end: u64) -> std::io::Result<()> {
if end <= start {
return Ok(());
}
let zeros = vec![0u8; (end - start) as usize];
#[allow(clippy::cast_possible_wrap)]
let mut off = start as libc::off_t;
let mut written = 0usize;
while written < zeros.len() {
let n = unsafe {
libc::pwrite(
fd,
zeros[written..].as_ptr().cast::<libc::c_void>(),
zeros.len() - written,
off,
)
};
if n < 0 {
return Err(std::io::Error::last_os_error());
}
if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"pwrite made no progress while zeroing range",
));
}
written += n as usize;
off += n as libc::off_t;
}
Ok(())
}
#[cfg(target_os = "linux")]
pub fn punch_hole(fd: RawFd, offset: u64, len: u64) -> std::io::Result<()> {
let mode = libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE;
#[allow(clippy::cast_possible_wrap)]
let (off, length) = (offset as libc::off_t, len as libc::off_t);
let ret = unsafe { libc::fallocate(fd, mode, off, length) };
if ret != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(target_os = "macos")]
pub fn punch_hole(fd: RawFd, offset: u64, len: u64) -> std::io::Result<()> {
#[allow(clippy::cast_possible_wrap)]
let mut ph = libc::fpunchhole_t {
fp_flags: 0,
reserved: 0,
fp_offset: offset as libc::off_t,
fp_length: len as libc::off_t,
};
let ret = unsafe { libc::fcntl(fd, libc::F_PUNCHHOLE, &mut ph) };
if ret != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub fn punch_hole(_fd: RawFd, _offset: u64, _len: u64) -> std::io::Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aligned_punch_range_skips_sub_block() {
assert_eq!(aligned_punch_range(0, 2048), None);
assert_eq!(aligned_punch_range(1024, 3072), None);
}
#[test]
fn aligned_punch_range_trims_edges_inward() {
assert_eq!(aligned_punch_range(2048, 10 * 1024), Some((4096, 4096)));
assert_eq!(aligned_punch_range(4096, 12288), Some((4096, 8192)));
}
#[cfg(unix)]
#[test]
fn punch_hole_reclaims_physical_blocks() {
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::AsRawFd;
let mut temp = tempfile::NamedTempFile::new().unwrap();
temp.write_all(&vec![0xABu8; 1024 * 1024]).unwrap();
temp.as_file().sync_all().unwrap();
let before = std::fs::metadata(temp.path()).unwrap().blocks() * 512;
assert!(
before >= 1024 * 1024,
"expected ~1 MiB allocated before punch, got {before}"
);
punch_hole(temp.as_file().as_raw_fd(), 0, 1024 * 1024).unwrap();
temp.as_file().sync_all().unwrap();
let after = std::fs::metadata(temp.path()).unwrap().blocks() * 512;
assert!(
after < 64 * 1024,
"expected the hole-punch to free the blocks, still {after} allocated"
);
}
#[cfg(unix)]
#[test]
#[allow(clippy::cast_possible_wrap)] fn zero_range_zeros_edges_and_reclaims_interior() {
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::AsRawFd;
let mut temp = tempfile::NamedTempFile::new().unwrap();
temp.write_all(&vec![0xCDu8; 2 * 1024 * 1024]).unwrap();
temp.as_file().sync_all().unwrap();
let fd = temp.as_file().as_raw_fd();
let before = std::fs::metadata(temp.path()).unwrap().blocks() * 512;
let (start, end) = (512u64, 1024 * 1024 + 512);
zero_range(fd, start, end).unwrap();
temp.as_file().sync_all().unwrap();
let mut buf = vec![0xFFu8; (end - start) as usize];
let n =
unsafe { libc::pread(fd, buf.as_mut_ptr().cast(), buf.len(), start as libc::off_t) };
assert_eq!(n, buf.len() as isize);
assert!(
buf.iter().all(|&b| b == 0),
"zeroed range must read back as zeros"
);
let mut tail = [0xFFu8; 512];
let n =
unsafe { libc::pread(fd, tail.as_mut_ptr().cast(), tail.len(), end as libc::off_t) };
assert_eq!(n, 512);
assert!(
tail.iter().all(|&b| b == 0xCD),
"data past the range is intact"
);
let after = std::fs::metadata(temp.path()).unwrap().blocks() * 512;
assert!(
after < before,
"interior should be reclaimed: before={before} after={after}"
);
}
}