use crate::fs_primitive_error::c_path;
use std::collections::BTreeMap;
use std::ffi::{CStr, CString};
use std::fs::{File, Metadata};
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::os::unix::io::AsRawFd;
use std::path::Path;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct AttributeLosses {
pub owner: u64,
pub extended_attributes: u64,
pub mode: u64,
pub times: u64,
}
#[derive(Debug)]
pub struct AttributeFailure {
pub action: &'static str,
pub error: std::io::Error,
}
#[derive(Clone, Copy)]
enum Handle<'a> {
Descriptor(&'a File),
Link(&'a CStr),
}
fn is_carried(name: &[u8]) -> bool {
return name.starts_with(b"user.") || name == b"system.posix_acl_access" || name == b"system.posix_acl_default";
}
fn attribute_names(handle: Handle<'_>) -> Vec<Vec<u8>> {
let length: isize = unsafe {
match handle {
Handle::Descriptor(file) => libc::flistxattr(file.as_raw_fd(), std::ptr::null_mut(), 0),
Handle::Link(path) => libc::llistxattr(path.as_ptr(), std::ptr::null_mut(), 0),
}
};
if length <= 0 {
return Vec::new();
}
let mut names: Vec<u8> = vec![0; length as usize];
let filled: isize = unsafe {
match handle {
Handle::Descriptor(file) => libc::flistxattr(file.as_raw_fd(), names.as_mut_ptr().cast(), names.len()),
Handle::Link(path) => libc::llistxattr(path.as_ptr(), names.as_mut_ptr().cast(), names.len()),
}
};
if filled <= 0 {
return Vec::new();
}
names.truncate(filled as usize);
return names
.split(|byte: &u8| { return *byte == 0; })
.filter(|name: &&[u8]| { return !name.is_empty(); })
.map(<[u8]>::to_vec)
.collect();
}
fn attribute_value(handle: Handle<'_>, name: &CStr) -> Option<Vec<u8>> {
let size: isize = unsafe {
match handle {
Handle::Descriptor(file) => libc::fgetxattr(file.as_raw_fd(), name.as_ptr(), std::ptr::null_mut(), 0),
Handle::Link(path) => libc::lgetxattr(path.as_ptr(), name.as_ptr(), std::ptr::null_mut(), 0),
}
};
if size < 0 {
return None;
}
let mut value: Vec<u8> = vec![0; size as usize];
let read: isize = unsafe {
match handle {
Handle::Descriptor(file) => libc::fgetxattr(file.as_raw_fd(), name.as_ptr(), value.as_mut_ptr().cast(), value.len()),
Handle::Link(path) => libc::lgetxattr(path.as_ptr(), name.as_ptr(), value.as_mut_ptr().cast(), value.len()),
}
};
if read < 0 {
return None;
}
value.truncate(read as usize);
return Some(value);
}
fn set_attribute(handle: Handle<'_>, name: &CStr, value: &[u8]) -> bool {
let result: libc::c_int = unsafe {
match handle {
Handle::Descriptor(file) => libc::fsetxattr(file.as_raw_fd(), name.as_ptr(), value.as_ptr().cast(), value.len(), 0),
Handle::Link(path) => libc::lsetxattr(path.as_ptr(), name.as_ptr(), value.as_ptr().cast(), value.len(), 0),
}
};
return result == 0;
}
fn remove_attribute(handle: Handle<'_>, name: &CStr) -> bool {
let result: libc::c_int = unsafe {
match handle {
Handle::Descriptor(file) => libc::fremovexattr(file.as_raw_fd(), name.as_ptr()),
Handle::Link(path) => libc::lremovexattr(path.as_ptr(), name.as_ptr()),
}
};
return result == 0;
}
fn copy_extended_attributes(source: &CStr, destination: Handle<'_>, remove_extras: bool, losses: &mut AttributeLosses) {
let wanted: Vec<Vec<u8>> = attribute_names(Handle::Link(source));
for name in &wanted {
let Ok(attribute) = CString::new(name.as_slice()) else { continue };
let Some(value) = attribute_value(Handle::Link(source), attribute.as_c_str()) else {
losses.extended_attributes += 1;
continue;
};
if !set_attribute(destination, attribute.as_c_str(), value.as_slice()) {
losses.extended_attributes += 1;
}
}
if !remove_extras {
return;
}
for name in attribute_names(destination) {
if !is_carried(name.as_slice()) || wanted.contains(&name) {
continue;
}
let Ok(attribute) = CString::new(name.as_slice()) else { continue };
if !remove_attribute(destination, attribute.as_c_str()) {
losses.extended_attributes += 1;
}
}
}
fn carried_attributes(path: &CStr) -> BTreeMap<Vec<u8>, Vec<u8>> {
let mut found: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
for name in attribute_names(Handle::Link(path)) {
if !is_carried(name.as_slice()) {
continue;
}
let Ok(attribute) = CString::new(name.as_slice()) else { continue };
if let Some(value) = attribute_value(Handle::Link(path), attribute.as_c_str()) {
found.insert(name, value);
}
}
return found;
}
pub fn carried_attributes_equal(source: &Path, destination: &Path) -> bool {
let (Ok(from), Ok(to)) = (c_path("llistxattr", source), c_path("llistxattr", destination)) else { return false };
return carried_attributes(from.as_c_str()) == carried_attributes(to.as_c_str());
}
fn is_missing_privilege(error: &std::io::Error) -> bool {
return matches!(error.raw_os_error(), Some(libc::EPERM | libc::EACCES));
}
fn settle(outcome: std::io::Result<()>, action: &'static str, refused: &mut u64) -> Result<(), AttributeFailure> {
let Err(error) = outcome else { return Ok(()) };
if !is_missing_privilege(&error) {
return Err(AttributeFailure { action, error });
}
*refused += 1;
return Ok(());
}
fn same_owner(source: (u32, u32), current: (u32, u32)) -> bool {
return source.0 == current.0 && source.1 == current.1;
}
fn set_owner(destination: Handle<'_>, current: &Metadata, source: &Metadata, losses: &mut AttributeLosses) {
if same_owner((source.uid(), source.gid()), (current.uid(), current.gid())) {
return;
}
let result: libc::c_int = unsafe {
match destination {
Handle::Descriptor(file) => libc::fchown(file.as_raw_fd(), source.uid(), source.gid()),
Handle::Link(path) => libc::lchown(path.as_ptr(), source.uid(), source.gid()),
}
};
if result != 0 {
losses.owner += 1;
}
}
fn set_times(destination: Handle<'_>, source: &Metadata) -> std::io::Result<()> {
let times: [libc::timespec; 2] = [
libc::timespec { tv_sec: source.atime(), tv_nsec: source.atime_nsec() },
libc::timespec { tv_sec: source.mtime(), tv_nsec: source.mtime_nsec() },
];
let result: libc::c_int = unsafe {
match destination {
Handle::Descriptor(file) => libc::futimens(file.as_raw_fd(), times.as_ptr()),
Handle::Link(path) => libc::utimensat(libc::AT_FDCWD, path.as_ptr(), times.as_ptr(), libc::AT_SYMLINK_NOFOLLOW),
}
};
if result != 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(());
}
fn apply(
source: &Path,
destination: Handle<'_>,
current: &Metadata,
metadata: &Metadata,
remove_extras: bool,
losses: &mut AttributeLosses,
) -> Result<(), AttributeFailure> {
if let Ok(from) = c_path("llistxattr", source) {
copy_extended_attributes(from.as_c_str(), destination, remove_extras, losses);
}
set_owner(destination, current, metadata, losses);
if let Handle::Descriptor(file) = destination {
let result: libc::c_int = unsafe { libc::fchmod(file.as_raw_fd(), metadata.mode() & 0o7777) };
let outcome: std::io::Result<()> = if result == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) };
settle(outcome, "set the permission bits of", &mut losses.mode)?;
}
settle(set_times(destination, metadata), "set the times of", &mut losses.times)?;
return Ok(());
}
pub fn preserve_on_descriptor(
source: &Path,
destination: &File,
metadata: &Metadata,
remove_extras: bool,
losses: &mut AttributeLosses,
) -> Result<(), AttributeFailure> {
let current: Metadata = destination.metadata().map_err(|error: std::io::Error| { return AttributeFailure { action: "examine", error }; })?;
return apply(source, Handle::Descriptor(destination), ¤t, metadata, remove_extras, losses);
}
pub fn preserve_on_link(source: &Path, destination: &Path, metadata: &Metadata, losses: &mut AttributeLosses) -> Result<(), AttributeFailure> {
let name: CString = c_path("lchown", destination).map_err(|error| { return AttributeFailure { action: "address", error: std::io::Error::from_raw_os_error(error.errno) }; })?;
let current: Metadata = std::fs::symlink_metadata(destination).map_err(|error: std::io::Error| { return AttributeFailure { action: "examine", error }; })?;
return apply(source, Handle::Link(name.as_c_str()), ¤t, metadata, false, losses);
}
pub fn open_fifo(path: &Path) -> std::io::Result<File> {
return std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW | libc::O_CLOEXEC).open(path);
}
pub fn make_fifo(path: &Path, mode: u32) -> std::io::Result<()> {
let name = c_path("mkfifo", path).map_err(|error| { return std::io::Error::from_raw_os_error(error.errno); })?;
let result: libc::c_int = unsafe { libc::mkfifo(name.as_ptr(), mode & 0o7777) };
if result != 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(());
}
#[cfg(test)]
#[path = "subvolume_sync_attrs_tests.rs"]
mod tests;