use crate::error::{FsError, Result};
use crate::handle::ReadOptions;
use crate::state::{EntryKind, StateDir};
use std::cell::Cell;
use std::fs::{self, File, OpenOptions};
use std::io;
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Clone, Copy, Debug)]
pub struct ConfinedCopy<'a> {
pub src_root: &'a Path,
pub dst_root: &'a Path,
}
impl<'a> ConfinedCopy<'a> {
pub const fn new(src_root: &'a Path, dst_root: &'a Path) -> Self {
Self { src_root, dst_root }
}
}
pub fn copy_tree_confined(src: &Path, dst: &Path, confine: &ConfinedCopy<'_>) -> Result<()> {
check_below(confine.src_root, src)?;
check_below(confine.dst_root, dst)?;
let real_src_root = canonical(confine.src_root)?;
let src_rel = src.strip_prefix(confine.src_root).unwrap_or(Path::new(""));
let real_src = real_src_root.join(src_rel);
let real_dst_root = canonical(confine.dst_root)?;
let dst_rel = dst.strip_prefix(confine.dst_root).unwrap_or(Path::new(""));
let real_dst = real_dst_root.join(dst_rel);
if real_dst.starts_with(&real_src) {
return Err(FsError::io(
dst,
io::Error::new(
io::ErrorKind::InvalidInput,
"the destination is the source or inside it",
),
));
}
let src_meta = refuse_links_below(&real_src_root, src_rel, false)?
.ok_or_else(|| FsError::io(src, io::Error::from(io::ErrorKind::NotFound)))?;
if refuse_links_below(&real_dst_root, dst_rel, true)?.is_some() {
return Err(FsError::AlreadyExists(dst.to_path_buf()));
}
let dst_name = real_dst
.file_name()
.ok_or_else(|| FsError::BadName(dst.display().to_string()))?
.to_os_string();
let dst_parent = real_dst
.parent()
.ok_or_else(|| FsError::BadName(dst.display().to_string()))?
.to_path_buf();
create_missing_parents(&real_dst_root, &dst_parent)?;
let src_name = real_src
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| FsError::BadName(src.display().to_string()))?
.to_string();
let src_parent = real_src
.parent()
.ok_or_else(|| FsError::BadName(src.display().to_string()))?;
let pinned_parent = StateDir::open_shared(src_parent)?;
let staging = staging_path(&dst_parent, &dst_name);
let created = Cell::new(false);
let published = (|| -> Result<()> {
if src_meta.is_dir() {
fs::create_dir(&staging).map_err(|e| FsError::io(&staging, e))?;
created.set(true);
let source = pinned_parent.raw_open_child(&src_name)?;
copy_dir_contents(&source, &staging)?;
set_mode_from(&real_src, &staging)?;
} else {
copy_one_file(&pinned_parent, &src_name, &staging, &created)?;
}
crate::atomic::rename_exclusive(&staging, &real_dst)
})();
if published.is_err() && created.get() {
remove_staging(&staging);
}
published
}
fn check_below(root: &Path, path: &Path) -> Result<()> {
let outside = || {
FsError::io(
path,
io::Error::new(
io::ErrorKind::InvalidInput,
format!("the path is not inside the root {}", root.display()),
),
)
};
let rel = path.strip_prefix(root).map_err(|_| outside())?;
if rel
.components()
.any(|c| !matches!(c, Component::Normal(_) | Component::CurDir))
{
return Err(outside());
}
Ok(())
}
fn canonical(path: &Path) -> Result<PathBuf> {
crate::plain::canonicalize_plain(path).map_err(|e| FsError::io(path, e))
}
fn is_link_like(meta: &fs::Metadata) -> bool {
if meta.file_type().is_symlink() {
return true;
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
if meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return true;
}
}
false
}
fn refuse_links_below(
root: &Path,
rel: &Path,
missing_is_ok: bool,
) -> Result<Option<fs::Metadata>> {
let mut current = root.to_path_buf();
let mut last = None;
let components: Vec<_> = rel
.components()
.filter(|c| matches!(c, Component::Normal(_)))
.collect();
for (index, component) in components.iter().enumerate() {
current.push(component);
match fs::symlink_metadata(¤t) {
Ok(meta) if is_link_like(&meta) => return Err(FsError::Symlink(current)),
Ok(meta) => {
if index + 1 < components.len() && !meta.is_dir() {
return Err(FsError::NotADirectory(current));
}
last = Some(meta);
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
if missing_is_ok {
return Ok(None);
}
return Err(FsError::io(¤t, e));
}
Err(e) => return Err(FsError::io(¤t, e)),
}
}
if components.is_empty() {
return fs::symlink_metadata(root)
.map(Some)
.map_err(|e| FsError::io(root, e));
}
Ok(last)
}
fn create_missing_parents(root: &Path, parent: &Path) -> Result<()> {
let rel = parent.strip_prefix(root).unwrap_or(Path::new(""));
let mut current = root.to_path_buf();
for component in rel.components() {
if let Component::Normal(part) = component {
current.push(part);
match fs::symlink_metadata(¤t) {
Ok(meta) if is_link_like(&meta) => return Err(FsError::Symlink(current)),
Ok(meta) if meta.is_dir() => {}
Ok(_) => return Err(FsError::NotADirectory(current)),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
fs::create_dir(¤t).map_err(|e| FsError::io(¤t, e))?;
}
Err(e) => return Err(FsError::io(¤t, e)),
}
}
}
Ok(())
}
fn staging_path(parent: &Path, name: &std::ffi::OsStr) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let name: String = name.to_string_lossy().chars().take(100).collect();
parent.join(format!(
".{name}.rk-confine-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
))
}
fn copy_dir_contents(source: &StateDir, target: &Path) -> Result<()> {
for entry in source.raw_entries()? {
let name = entry.name.as_str();
let dst = target.join(name);
match entry.kind {
EntryKind::Dir => {
fs::create_dir(&dst).map_err(|e| FsError::io(&dst, e))?;
let child = source.raw_open_child(name)?;
copy_dir_contents(&child, &dst)?;
set_mode_from(&source.path().join(name), &dst)?;
}
EntryKind::File => copy_one_file(source, name, &dst, &Cell::new(false))?,
EntryKind::Symlink => return Err(FsError::Symlink(source.path().join(name))),
EntryKind::Other => return Err(FsError::NotAFile(source.path().join(name))),
}
}
Ok(())
}
fn copy_one_file(source: &StateDir, name: &str, dst: &Path, created: &Cell<bool>) -> Result<()> {
let mut input = source.open_read(name, &ReadOptions::new())?;
let meta = input
.metadata()
.map_err(|e| FsError::io(source.path().join(name), e))?;
if !meta.is_file() {
return Err(FsError::NotAFile(source.path().join(name)));
}
let mut open = OpenOptions::new();
open.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
open.mode(meta.permissions().mode() & 0o7777);
}
let mut output: File = open.open(dst).map_err(|e| FsError::io(dst, e))?;
created.set(true);
io::copy(&mut input, &mut output).map_err(|e| FsError::io(dst, e))?;
output
.set_permissions(meta.permissions())
.map_err(|e| FsError::io(dst, e))?;
Ok(())
}
fn set_mode_from(src_path: &Path, dst: &Path) -> Result<()> {
let perms = fs::metadata(src_path)
.map_err(|e| FsError::io(src_path, e))?
.permissions();
fs::set_permissions(dst, perms).map_err(|e| FsError::io(dst, e))
}
fn remove_staging(staging: &Path) {
fn make_writable(dir: &Path) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let Ok(kind) = entry.file_type() else {
continue;
};
if kind.is_dir() {
let path = entry.path();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o700));
}
make_writable(&path);
}
}
}
if fs::symlink_metadata(staging).is_ok_and(|m| m.is_dir() && !is_link_like(&m)) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(staging, fs::Permissions::from_mode(0o700));
}
make_writable(staging);
}
let _ = crate::tree::remove_tree(staging, &crate::tree::RemoveOptions::new());
}