use std::io::{Read, Write};
use std::path::Path;
use tempfile::NamedTempFile;
use super::digest::{Digest256, verify_blob_file};
pub(super) fn replace_file_atomic(target: &Path, bytes: &[u8]) -> crate::Result<()> {
let parent = target.parent().ok_or_else(|| {
crate::new_error!("atomic write: target {:?} has no parent directory", target)
})?;
let mut tmp = NamedTempFile::new_in(parent).map_err(|e| {
crate::new_error!("atomic write: failed to create tmp in {:?}: {}", parent, e)
})?;
tmp.write_all(bytes).map_err(|e| {
crate::new_error!("atomic write: failed to write tmp {:?}: {}", tmp.path(), e)
})?;
tmp.as_file_mut().sync_all().map_err(|e| {
crate::new_error!("atomic write: failed to sync tmp {:?}: {}", tmp.path(), e)
})?;
tmp.persist(target).map_err(|e| {
crate::new_error!("atomic write: failed to persist tmp to {:?}: {}", target, e)
})?;
Ok(())
}
pub(super) fn put_blob(blobs_dir: &Path, digest: &Digest256, bytes: &[u8]) -> crate::Result<()> {
replace_file_atomic(&blobs_dir.join(&digest.hex), bytes)
}
pub(super) fn put_blob_if_absent(
blobs_dir: &Path,
digest: &Digest256,
bytes: &[u8],
) -> crate::Result<()> {
let target = blobs_dir.join(&digest.hex);
if let Ok(meta) = std::fs::symlink_metadata(&target)
&& meta.is_file()
&& meta.len() == bytes.len() as u64
&& let Ok(mut file) = std::fs::File::open(&target)
&& verify_blob_file("existing snapshot", &mut file, &digest.hex).is_ok()
{
return Ok(());
}
replace_file_atomic(&target, bytes)
}
#[cfg(not(unix))]
pub(super) fn reject_symlink(path: &Path) -> crate::Result<()> {
let meta = match std::fs::symlink_metadata(path) {
Ok(meta) => meta,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(crate::new_error!("failed to stat {:?}: {}", path, e)),
};
if meta.file_type().is_symlink() {
return Err(crate::new_error!(
"{:?} is a symbolic link; refusing to follow it",
path
));
}
Ok(())
}
pub(super) fn open_no_follow(path: &Path) -> crate::Result<std::fs::File> {
#[cfg(unix)]
let opened = {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
};
#[cfg(not(unix))]
let opened = {
reject_symlink(path)?;
std::fs::File::open(path)
};
opened.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
crate::new_error!("blob file {:?} not found", path)
} else {
crate::new_error!("failed to open {:?}: {}", path, e)
}
})
}
pub(super) fn read_bounded(path: &Path, max_size: u64) -> crate::Result<Vec<u8>> {
let f = open_no_follow(path)?;
let hint = f.metadata().map(|m| m.len().min(max_size)).unwrap_or(0);
let mut buf = Vec::with_capacity(hint as usize);
f.take(max_size.saturating_add(1))
.read_to_end(&mut buf)
.map_err(|e| crate::new_error!("failed to read {:?}: {}", path, e))?;
if buf.len() as u64 > max_size {
return Err(crate::new_error!(
"file {:?} exceeds maximum allowed {} bytes",
path,
max_size
));
}
Ok(buf)
}