use super::*;
use std::ffi::{CString, OsStr};
use std::fs::{File, OpenOptions};
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::macos::fs::MetadataExt as MacMetadataExt;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
unsafe extern "C" {
fn openat(directory: i32, path: *const std::ffi::c_char, flags: i32, ...) -> i32;
fn mkdirat(directory: i32, path: *const std::ffi::c_char, mode: u16) -> i32;
fn fcopyfile(source: i32, destination: i32, state: *mut std::ffi::c_void, flags: u32) -> i32;
}
const DIRECTORY: i32 = 0x00100000;
const NOFOLLOW: i32 = 0x20000000;
const CLOEXEC: i32 = 0x01000000;
const NONBLOCK: i32 = 4;
impl MutationPlatform for MacFiles {
fn open_link(&self, path: &Path) -> Result<File, FileError> {
cueward_core::files::relocation::RelocationPlatform::open_symlink(self, path)
}
fn create_link(&self, parent: &File, name: &OsStr, target: &str) -> Creation {
objects::create_link(parent, name, target)
}
fn copy_names(
&self,
file: &File,
maximum: usize,
) -> Result<Vec<std::ffi::OsString>, FileError> {
cueward_core::files::copy_tree::CopyTreePlatform::tree_names(self, file, maximum)
}
fn read_copy_attributes(&self, file: &File) -> Result<(String, usize), FileError> {
attributes::digest(file)
}
fn prepare_staging(
&self,
receipt: &MutationReceipt,
root: &File,
) -> Result<Staging, FileError> {
publication::prepare(receipt, root)
}
fn publish(&self, root: &File, staging: &Staging, destination: &Path) -> Publication {
publication::publish(root, staging, destination)
}
fn open_directory(&self, path: &Path) -> Result<File, FileError> {
Ok(OpenOptions::new()
.read(true)
.custom_flags(DIRECTORY | NOFOLLOW | NONBLOCK | CLOEXEC)
.open(path)?)
}
fn create_file(&self, parent: &File, name: &OsStr) -> Creation {
let name = match leaf(name) {
Ok(name) => name,
Err(error) => return creation(Err(error), Some(false)),
};
let fd = unsafe {
openat(
parent.as_raw_fd(),
name.as_ptr(),
2 | 0x200 | 0x800 | NOFOLLOW | NONBLOCK | CLOEXEC,
0o600u32,
)
};
creation(descriptor(fd), None)
}
fn create_directory(&self, parent: &File, name: &OsStr) -> Creation {
let name = match leaf(name) {
Ok(name) => name,
Err(error) => return creation(Err(error), Some(false)),
};
if unsafe { mkdirat(parent.as_raw_fd(), name.as_ptr(), 0o700) } != 0 {
return creation(Err(std::io::Error::last_os_error().into()), None);
}
creation(
descriptor(unsafe {
openat(
parent.as_raw_fd(),
name.as_ptr(),
DIRECTORY | NOFOLLOW | NONBLOCK | CLOEXEC,
)
}),
Some(true),
)
}
fn validate_copy_source(&self, file: &File, info: &FileInfo) -> Result<(), FileError> {
let metadata = file.metadata()?;
if metadata.mode() & 0o7000 != 0 || metadata.st_flags() & 0x20 != 0 {
return Err(FileError::new(
FileErrorCode::UnsupportedType,
"special permission bits and compressed sources are not supported by verified copying",
));
}
if info.kind == FileKind::Symlink {
attributes::digest(file)?;
return Ok(());
}
let resources = super::super::metadata::inspect(Path::new(&info.path), &metadata)?;
match resources.is_alias_file {
ResourceValue::Available { .. } => {}
_ => {
return Err(FileError::new(
FileErrorCode::Unavailable,
"cannot establish copy source alias state",
));
}
}
attributes::digest(file)?;
Ok(())
}
fn copy_attributes(
&self,
source: &File,
destination: &File,
) -> Result<(String, usize), FileError> {
let before = attributes::digest(source)?;
if unsafe {
fcopyfile(
source.as_raw_fd(),
destination.as_raw_fd(),
std::ptr::null_mut(),
4,
)
} != 0
{
return Err(std::io::Error::last_os_error().into());
}
if before != attributes::digest(source)? || before != attributes::digest(destination)? {
return Err(FileError::new(
FileErrorCode::VerificationFailed,
"extended attributes changed or did not match after copying",
));
}
Ok(before)
}
}
pub(super) fn leaf(name: &OsStr) -> Result<CString, FileError> {
let bytes = name.as_bytes();
if bytes.is_empty() || bytes == b"." || bytes == b".." || bytes.contains(&b'/') {
return Err(FileError::new(
FileErrorCode::InvalidOptions,
"creation requires one normal leaf name",
));
}
CString::new(bytes)
.map_err(|_| FileError::new(FileErrorCode::InvalidOptions, "NUL in leaf name"))
}
fn descriptor(fd: i32) -> Result<File, FileError> {
if fd < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(unsafe { File::from_raw_fd(fd) })
}
fn creation(file: Result<File, FileError>, known: Option<bool>) -> Creation {
let destination_created = if file.is_ok() {
Some(true)
} else if known.is_some() {
known
} else if file.as_ref().is_err_and(|e| {
matches!(
e.code,
FileErrorCode::Conflict | FileErrorCode::PermissionDenied | FileErrorCode::NotFound
)
}) {
Some(false)
} else {
None
};
Creation {
file,
destination_created,
}
}