use anyhow::{bail, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
static TMP_COUNTER: AtomicU64 = AtomicU64::new(0);
#[cfg(target_os = "macos")]
mod macos_sys {
use std::os::raw::{c_char, c_int};
extern "C" {
pub fn clonefile(src: *const c_char, dst: *const c_char, flags: u32) -> c_int;
}
}
#[cfg(target_os = "linux")]
mod linux_sys {
use std::os::raw::{c_int, c_ulong};
pub const FICLONE: c_ulong = 0x40049409;
extern "C" {
pub fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoreHardlinks {
Never,
Fallback,
Always,
}
fn staged_tmp(dst: &Path, tag: &str) -> Result<PathBuf> {
let parent = dst
.parent()
.context("Destination path must have a parent directory")?;
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create parent directory: {:?}", parent))?;
let file_name = dst
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("artifact");
let counter = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
Ok(parent.join(format!(
".{}.{}.{}.{}",
file_name,
tag,
std::process::id(),
counter
)))
}
fn try_reflink(src: &Path, tmp_dst: &Path) -> bool {
if std::env::var_os("MITTS_NO_REFLINK").is_some() {
return false;
}
#[cfg(target_os = "macos")]
{
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
if let (Ok(c_src), Ok(c_tmp)) = (
CString::new(src.as_os_str().as_bytes()),
CString::new(tmp_dst.as_os_str().as_bytes()),
) {
return unsafe { macos_sys::clonefile(c_src.as_ptr(), c_tmp.as_ptr(), 0) } == 0;
}
false
}
#[cfg(target_os = "linux")]
{
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
if let (Ok(src_file), Ok(tmp_file)) = (
OpenOptions::new().read(true).open(src),
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(tmp_dst),
) {
let ret = unsafe {
linux_sys::ioctl(
tmp_file.as_raw_fd(),
linux_sys::FICLONE,
src_file.as_raw_fd(),
)
};
if ret == 0 {
return true;
}
let _ = fs::remove_file(tmp_dst);
}
false
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
let _ = (src, tmp_dst);
false
}
}
fn try_store_hardlink(src: &Path, tmp_dst: &Path, executable: bool) -> bool {
if fs::hard_link(src, tmp_dst).is_err() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = if executable { 0o555 } else { 0o444 };
if fs::metadata(tmp_dst)
.map(|m| m.permissions().mode() & 0o777 != mode)
.unwrap_or(true)
{
let _ = fs::set_permissions(tmp_dst, fs::Permissions::from_mode(mode));
}
}
true
}
fn promote(tmp_dst: &Path, dst: &Path) -> Result<()> {
if let Err(rename_err) = fs::rename(tmp_dst, dst) {
let _ = fs::remove_file(tmp_dst);
bail!(
"Failed to atomically stage file to {:?}: {}",
dst,
rename_err
);
}
Ok(())
}
pub fn reflink_or_copy(src: &Path, dst: &Path, executable: bool) -> Result<()> {
restore_store_object(src, dst, executable, StoreHardlinks::Never)
}
pub fn restore_store_object(
src: &Path,
dst: &Path,
executable: bool,
policy: StoreHardlinks,
) -> Result<()> {
let tmp_dst = staged_tmp(dst, "tmp")?;
let file_size = fs::metadata(src).map(|m| m.len()).unwrap_or(0);
if policy == StoreHardlinks::Always && try_store_hardlink(src, &tmp_dst, executable) {
crate::core::metrics::record_hardlinked(file_size);
return promote(&tmp_dst, dst);
}
if try_reflink(src, &tmp_dst) {
crate::core::metrics::record_cloned(file_size);
} else if policy == StoreHardlinks::Fallback && try_store_hardlink(src, &tmp_dst, executable) {
crate::core::metrics::record_hardlinked(file_size);
return promote(&tmp_dst, dst);
} else {
fs::copy(src, &tmp_dst)
.with_context(|| format!("Failed to copy bytes from {:?} to {:?}", src, tmp_dst))?;
crate::core::metrics::record_copied(file_size);
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = if executable { 0o755 } else { 0o644 };
let _ = fs::set_permissions(&tmp_dst, fs::Permissions::from_mode(mode));
}
promote(&tmp_dst, dst)
}
pub fn reflink_or_copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
if !src.exists() {
return Ok(());
}
fs::create_dir_all(dst)
.with_context(|| format!("Failed to create destination dir: {:?}", dst))?;
for entry in fs::read_dir(src).with_context(|| format!("Failed to read dir: {:?}", src))? {
let entry = entry?;
let entry_path = entry.path();
let target_path = dst.join(entry.file_name());
let file_type = entry.file_type()?;
if file_type.is_dir() {
reflink_or_copy_dir_recursive(&entry_path, &target_path)?;
} else if file_type.is_file() {
#[cfg(unix)]
let executable = {
use std::os::unix::fs::PermissionsExt;
entry
.metadata()
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
};
#[cfg(not(unix))]
let executable = false;
reflink_or_copy(&entry_path, &target_path, executable)?;
}
}
Ok(())
}
fn canonicalize_leaf_containment(path: &Path) -> PathBuf {
if let Some(parent) = path.parent() {
if let Ok(c_parent) = parent.canonicalize() {
if let Some(file_name) = path.file_name() {
return c_parent.join(file_name);
}
return c_parent;
}
}
path.canonicalize().unwrap_or_else(|_| {
if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(path))
.unwrap_or_else(|_| path.to_path_buf())
}
})
}
fn canonicalize_root(root: &Path) -> PathBuf {
root.canonicalize().unwrap_or_else(|_| {
if root.is_absolute() {
root.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(root))
.unwrap_or_else(|_| root.to_path_buf())
}
})
}
pub fn is_path_under_roots(path: &Path, allowed_roots: &[&Path]) -> bool {
let canonical = canonicalize_leaf_containment(path);
for root in allowed_roots {
let canonical_root = canonicalize_root(root);
if canonical.starts_with(&canonical_root) {
return true;
}
}
false
}
pub fn link_intra_workspace(
src: &Path,
dst: &Path,
allowed_roots: &[&Path],
executable: bool,
) -> Result<()> {
if src == dst {
return Ok(());
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if let (Ok(src_meta), Ok(dst_meta)) = (fs::symlink_metadata(src), fs::symlink_metadata(dst))
{
if src_meta.dev() == dst_meta.dev() && src_meta.ino() == dst_meta.ino() {
return Ok(());
}
}
}
let parent = dst
.parent()
.context("Destination path must have a parent directory")?;
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create parent directory for {:?}", dst))?;
let file_name = dst
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("artifact");
let pid = std::process::id();
let counter = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let tmp_dst = parent.join(format!(".{}.tmp_link.{}.{}", file_name, pid, counter));
let mut hardlink_succeeded = false;
#[cfg(unix)]
{
let disable_hardlinks = std::env::var_os("MITTS_NO_HARDLINK").is_some();
let src_is_regular_file = fs::symlink_metadata(src)
.map(|m| m.file_type().is_file())
.unwrap_or(false);
if !disable_hardlinks
&& src_is_regular_file
&& is_path_under_roots(src, allowed_roots)
&& is_path_under_roots(dst, allowed_roots)
{
let _ = fs::remove_file(&tmp_dst);
if fs::hard_link(src, &tmp_dst).is_ok() {
hardlink_succeeded = true;
}
}
}
if hardlink_succeeded {
if let Err(rename_err) = fs::rename(&tmp_dst, dst) {
let _ = fs::remove_file(&tmp_dst);
bail!(
"Failed to atomically stage hardlink to {:?}: {}",
dst,
rename_err
);
}
let file_size = fs::metadata(dst).map(|m| m.len()).unwrap_or(0);
crate::core::metrics::record_hardlinked(file_size);
return Ok(());
}
reflink_or_copy(src, dst, executable)
}