use super::*;
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) fn rename_noreplace(parent: &CapDir, source: &OsStr, target: &OsStr) -> io::Result<()> {
use std::{
ffi::CString,
os::{fd::AsRawFd, unix::ffi::OsStrExt},
};
let source_c = CString::new(source.as_bytes())
.map_err(|_| io::Error::new(ErrorKind::InvalidInput, "rename source contains NUL"))?;
let target_c = CString::new(target.as_bytes())
.map_err(|_| io::Error::new(ErrorKind::InvalidInput, "rename target contains NUL"))?;
#[cfg(target_os = "linux")]
{
let result = unsafe {
libc::renameat2(
parent.as_raw_fd(),
source_c.as_ptr(),
parent.as_raw_fd(),
target_c.as_ptr(),
libc::RENAME_NOREPLACE,
)
};
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(target_os = "macos")]
{
let result = unsafe {
libc::renameatx_np(
parent.as_raw_fd(),
source_c.as_ptr(),
parent.as_raw_fd(),
target_c.as_ptr(),
libc::RENAME_EXCL,
)
};
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub(super) fn rename_noreplace(parent: &CapDir, source: &OsStr, target: &OsStr) -> io::Result<()> {
let _ = (parent, source, target);
Err(io::Error::new(
ErrorKind::Unsupported,
"atomic no-replace rename is unsupported on this platform",
))
}
pub(super) fn open_rewind_root(cwd: &Path) -> (PathBuf, Option<CapDir>) {
let canonical_cwd = cwd
.canonicalize()
.unwrap_or_else(|_| lexical_normalize(cwd));
let root = CapDir::open_ambient_dir(&canonical_cwd, ambient_authority()).ok();
(canonical_cwd, root)
}
#[derive(Debug)]
pub(super) struct RewindTarget {
pub(super) parent: CapDir,
pub(super) name: OsString,
}
#[derive(Debug)]
pub(super) struct TargetSnapshot {
pub(super) hash: String,
#[cfg(unix)]
pub(super) mode: u32,
}
pub(super) fn open_target_parent(
root: &CapDir,
relative_path: &Path,
create_missing: bool,
inject_ancestor_sync_failure: bool,
) -> io::Result<RewindTarget> {
let mut components = Vec::new();
for component in relative_path.components() {
match component {
Component::Normal(name) => components.push(name),
Component::CurDir => {}
Component::ParentDir | Component::Prefix(_) | Component::RootDir => {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"unsafe rewind target path",
));
}
}
}
let name = components
.pop()
.ok_or_else(|| io::Error::new(ErrorKind::InvalidInput, "rewind target has no name"))?;
let mut parent = root.try_clone()?;
for component in components {
parent = open_parent_component(
&parent,
component,
create_missing,
inject_ancestor_sync_failure,
)?;
}
Ok(RewindTarget {
parent,
name: name.to_os_string(),
})
}
fn open_parent_component(
parent: &CapDir,
name: &OsStr,
create_missing: bool,
inject_ancestor_sync_failure: bool,
) -> io::Result<CapDir> {
let component = Path::new(name);
match parent.symlink_metadata(component) {
Ok(metadata) => {
if metadata.is_symlink() || !metadata.is_dir() {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"rewind target parent is not a safe directory",
));
}
parent.open_dir_nofollow(component)
}
Err(error) if error.kind() == ErrorKind::NotFound && create_missing => {
let created = match parent.create_dir(component) {
Ok(()) => true,
Err(error) if error.kind() == ErrorKind::AlreadyExists => false,
Err(error) => return Err(error),
};
if created {
let sync_result = if inject_ancestor_sync_failure {
Err(io::Error::other("injected ancestor directory sync failure"))
} else {
sync_capability_directory(parent)
};
if let Err(error) = sync_result {
let _ = parent.remove_dir(component);
return Err(error);
}
}
parent.open_dir_nofollow(component)
}
Err(error) => Err(error),
}
}
pub(super) fn target_snapshot(target: &RewindTarget) -> io::Result<Option<TargetSnapshot>> {
snapshot_named(&target.parent, &target.name)
}
pub(super) fn snapshot_named(parent: &CapDir, name: &OsStr) -> io::Result<Option<TargetSnapshot>> {
let metadata = match parent.symlink_metadata(name) {
Ok(metadata) => metadata,
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
if metadata.is_symlink() {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"rewind target is a symlink",
));
}
if !metadata.is_file() {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"rewind target is not a regular file",
));
}
let mut options = CapOpenOptions::new();
options.read(true).follow(FollowSymlinks::No);
let mut file = match parent.open_with(name, &options) {
Ok(file) => file,
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
let metadata = file.metadata()?;
if metadata.is_symlink() || !metadata.is_file() {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"rewind target is not a regular file",
));
}
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
#[cfg(unix)]
let mode = metadata.permissions().mode() & 0o777;
Ok(Some(TargetSnapshot {
hash: sha256_hex(&bytes),
#[cfg(unix)]
mode,
}))
}
pub(super) fn target_snapshot_for_plan(
root: &CapDir,
relative_path: &Path,
missing_parent_is_missing: bool,
) -> anyhow::Result<Option<TargetSnapshot>> {
let target = match open_target_parent(root, relative_path, false, false) {
Ok(target) => target,
Err(error) if missing_parent_is_missing && error.kind() == ErrorKind::NotFound => {
return Ok(None);
}
Err(error) => return Err(error.into()),
};
Ok(target_snapshot(&target)?)
}
pub(super) fn target_mode(snapshot: &TargetSnapshot) -> Option<u32> {
#[cfg(unix)]
{
Some(snapshot.mode)
}
#[cfg(not(unix))]
{
let _ = snapshot;
None
}
}
pub(super) fn reject_final_symlink(parent: &CapDir, name: &OsStr) -> io::Result<()> {
match parent.symlink_metadata(Path::new(name)) {
Ok(metadata) if metadata.is_symlink() => Err(io::Error::new(
ErrorKind::InvalidInput,
"rewind target is a symlink",
)),
Ok(_) => Ok(()),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}
pub(super) fn sync_capability_directory(directory: &CapDir) -> io::Result<()> {
#[cfg(unix)]
{
directory.try_clone()?.into_std_file().sync_all()
}
#[cfg(not(unix))]
{
let _ = directory;
Ok(())
}
}