use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use super::{LeanError, LeanResult};
fn refuse(path: &Path, why: &str) -> LeanError {
LeanError::State(format!("refusing write to {}: {why} (containment)", path.display()))
}
pub(crate) fn open_beneath_nofollow(root: &Path, rel: &str) -> std::io::Result<std::fs::File> {
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::fs::OpenOptionsExt;
let invalid = |why: &str| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{rel}: {why}"));
let comps: Vec<&str> = rel.split('/').filter(|c| !c.is_empty()).collect();
if comps.is_empty() || comps.iter().any(|c| *c == "." || *c == "..") {
return Err(invalid("not a plain relative path"));
}
let mut dir = OpenOptions::new().read(true).custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC).open(root)?;
for (i, c) in comps.iter().enumerate() {
let last = i + 1 == comps.len();
let name = std::ffi::CString::new(*c).map_err(|_| invalid("a component holds a NUL"))?;
let flags = libc::O_RDONLY
| libc::O_NOFOLLOW
| libc::O_CLOEXEC
| if last { libc::O_NONBLOCK } else { libc::O_DIRECTORY };
let fd = unsafe { libc::openat(dir.as_raw_fd(), name.as_ptr(), flags) };
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
let f = unsafe { std::fs::File::from_raw_fd(fd) };
if last {
if !f.metadata()?.is_file() {
return Err(invalid("not a regular file"));
}
return Ok(f);
}
dir = f;
}
unreachable!("the loop returns at the last component")
}
pub(crate) fn check_parent(path: &Path) -> LeanResult<()> {
let Some(parent) = path.parent() else { return Ok(()) };
if let Ok(m) = std::fs::symlink_metadata(parent) {
if m.file_type().is_symlink() {
return Err(refuse(path, "parent directory is a symlink"));
}
}
Ok(())
}
pub(crate) fn write_via_tmp(
path: &Path,
tmp: &Path,
bytes: &[u8],
mode: Option<u32>,
) -> LeanResult<std::fs::Metadata> {
write_via_tmp_opts(path, tmp, bytes, mode, true, false, None).map(|m| m.expect("unguarded"))
}
pub(crate) fn write_via_tmp_fast(
path: &Path,
tmp: &Path,
bytes: &[u8],
mode: Option<u32>,
) -> LeanResult<std::fs::Metadata> {
write_via_tmp_opts(path, tmp, bytes, mode, false, true, None).map(|m| m.expect("unguarded"))
}
pub(crate) fn write_via_tmp_fast_if(
path: &Path,
tmp: &Path,
bytes: &[u8],
mode: Option<u32>,
proceed: &dyn Fn() -> bool,
) -> LeanResult<Option<std::fs::Metadata>> {
write_via_tmp_opts(path, tmp, bytes, mode, false, true, Some(proceed))
}
fn create_exclusive(tmp: &Path, mkdir_parent: bool) -> LeanResult<std::fs::File> {
let open = || OpenOptions::new().write(true).create_new(true).open(tmp);
let first = match open() {
Ok(f) => return Ok(f),
Err(e) => e,
};
let again = |why: &str, e: std::io::Error| {
refuse(tmp, &format!("temp file is not exclusively creatable after {why}: {e}"))
};
match first.kind() {
std::io::ErrorKind::AlreadyExists => {
if let Err(e) = std::fs::remove_file(tmp) {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(refuse(tmp, &format!("stale temp file is not removable: {e}")));
}
}
open().map_err(|e| again("removing a leftover", e))
}
std::io::ErrorKind::NotFound if mkdir_parent => {
if let Some(parent) = tmp.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| refuse(tmp, &format!("mkdir for temp: {e}")))?;
}
open().map_err(|e| again("recreating its parent", e))
}
_ => Err(refuse(tmp, &format!("temp file is not exclusively creatable: {first}"))),
}
}
pub(crate) struct RangedTmp {
file: std::fs::File,
tmp: std::path::PathBuf,
path: std::path::PathBuf,
written: std::sync::atomic::AtomicU64,
expect: u64,
}
impl RangedTmp {
pub(crate) fn create(
path: &Path,
tmp: &Path,
size: u64,
mode: Option<u32>,
) -> LeanResult<RangedTmp> {
let f = create_exclusive(tmp, true)?;
#[cfg(unix)]
if let Some(mode) = mode {
use std::os::unix::fs::PermissionsExt;
f.set_permissions(std::fs::Permissions::from_mode(mode))
.map_err(|e| refuse(tmp, &format!("mode: {e}")))?;
}
#[cfg(not(unix))]
let _ = mode;
Ok(RangedTmp {
file: f,
tmp: tmp.to_path_buf(),
path: path.to_path_buf(),
written: std::sync::atomic::AtomicU64::new(0),
expect: size,
})
}
pub(crate) fn write_at(&self, offset: u64, bytes: &[u8]) -> LeanResult<()> {
#[cfg(unix)]
{
use std::os::unix::fs::FileExt;
self.file
.write_all_at(bytes, offset)
.map_err(|e| refuse(&self.tmp, &format!("write at {offset}: {e}")))?;
}
#[cfg(not(unix))]
{
use std::io::{Seek, SeekFrom, Write};
let mut f = &self.file;
f.seek(SeekFrom::Start(offset))
.map_err(|e| refuse(&self.tmp, &format!("seek to {offset}: {e}")))?;
f.write_all(bytes)
.map_err(|e| refuse(&self.tmp, &format!("write at {offset}: {e}")))?;
}
self.written
.fetch_add(bytes.len() as u64, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
pub(crate) fn commit(self) -> LeanResult<()> {
let written = self.written.load(std::sync::atomic::Ordering::Relaxed);
if written != self.expect {
let _ = std::fs::remove_file(&self.tmp);
return Err(LeanError::State(format!(
"ranged materialisation of {} wrote {} of {} bytes — refusing to rename a \
file with a hole in it",
self.path.display(),
written,
self.expect
)));
}
drop(self.file);
std::fs::rename(&self.tmp, &self.path).map_err(|e| {
let _ = std::fs::remove_file(&self.tmp);
refuse(&self.path, &format!("rename from temp: {e}"))
})
}
}
pub(crate) fn leave_the_reserve(root: &Path, rel: &str, bytes: u64, room: Option<u64>) -> LeanResult<()> {
let (room, block) = match room {
Some(r) => (r, 4096),
None => {
let st = rustix::fs::statvfs(root).map_err(std::io::Error::from)?;
let block = st.f_frsize.max(1);
(st.f_bavail.saturating_mul(block), block)
}
};
let need = bytes_with_slack(bytes, block);
if need > room {
return Err(LeanError::Io(std::io::Error::new(
std::io::ErrorKind::StorageFull,
format!(
"{rel}: its {bytes} bytes would land in the syncer's reserve: the tree has {room} bytes left \
for its tenant, and the reserve is kept for the syncer's own bookkeeping (the tree is full)"
),
)));
}
Ok(())
}
pub(crate) fn bytes_with_slack(bytes: u64, block: u64) -> u64 {
let block = block.max(1);
bytes.div_ceil(block).saturating_add(4).saturating_mul(block)
}
pub(crate) fn sync_tree(dir: &Path) -> LeanResult<()> {
let d = std::fs::File::open(dir)
.map_err(|e| LeanError::State(format!("open {} to sync: {e}", dir.display())))?;
#[cfg(target_os = "linux")]
{
use std::os::fd::AsRawFd;
if unsafe { libc::syncfs(d.as_raw_fd()) } != 0 {
let e = std::io::Error::last_os_error();
return Err(LeanError::State(format!("syncfs {}: {e}", dir.display())));
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
{
d.sync_all()
.map_err(|e| LeanError::State(format!("fsync {}: {e}", dir.display())))
}
}
fn write_via_tmp_opts(
path: &Path,
tmp: &Path,
bytes: &[u8],
mode: Option<u32>,
durable: bool,
mkdir_parent: bool,
proceed: Option<&dyn Fn() -> bool>,
) -> LeanResult<Option<std::fs::Metadata>> {
let mut f = create_exclusive(tmp, mkdir_parent)?;
#[cfg(unix)]
if let Some(mode) = mode {
use std::os::unix::fs::PermissionsExt;
let _ = f.set_permissions(std::fs::Permissions::from_mode(mode & 0o7777));
}
#[cfg(not(unix))]
let _ = mode;
f.write_all(bytes)
.map_err(|e| LeanError::State(format!("write tmp for {}: {e}", path.display())))?;
if durable {
f.sync_all()
.map_err(|e| LeanError::State(format!("fsync tmp for {}: {e}", path.display())))?;
}
let meta = f
.metadata()
.map_err(|e| LeanError::State(format!("fstat tmp for {}: {e}", path.display())))?;
drop(f);
if proceed.is_some_and(|p| !p()) {
let _ = std::fs::remove_file(tmp);
return Ok(None);
}
std::fs::rename(tmp, path)
.map_err(|e| LeanError::State(format!("rename into {}: {e}", path.display())))?;
if durable {
if let Some(parent) = path.parent() {
if let Ok(d) = std::fs::File::open(parent) {
let _ = d.sync_all();
}
}
}
Ok(Some(meta))
}