use crate::error::{FsError, Result};
use std::fs::File;
use std::path::Path;
#[cfg(target_os = "macos")]
mod sys {
use std::ffi::{c_int, c_uint, c_void};
use std::fs::File;
use std::io;
use std::os::fd::AsRawFd;
type Acl = *mut c_void;
type Entry = *mut c_void;
type Flagset = *mut c_void;
const ACL_TYPE_EXTENDED: c_int = 0x0000_0100;
const ACL_FIRST_ENTRY: c_int = 0;
const ACL_NEXT_ENTRY: c_int = -1;
const ACL_EXTENDED_DENY: c_int = 2;
const ACL_ENTRY_FILE_INHERIT: c_uint = 1 << 5;
const ACL_ENTRY_DIRECTORY_INHERIT: c_uint = 1 << 6;
extern "C" {
fn acl_get_fd_np(fd: c_int, acl_type: c_int) -> Acl;
fn acl_get_link_np(path: *const std::ffi::c_char, acl_type: c_int) -> Acl;
fn acl_free(object: *mut c_void) -> c_int;
fn acl_get_entry(acl: Acl, entry_id: c_int, entry: *mut Entry) -> c_int;
fn acl_get_tag_type(entry: Entry, tag: *mut c_int) -> c_int;
fn acl_get_flagset_np(object: *mut c_void, flagset: *mut Flagset) -> c_int;
fn acl_get_flag_np(flagset: Flagset, flag: c_uint) -> c_int;
}
fn read(file: &File) -> io::Result<Option<Acl>> {
let acl = unsafe { acl_get_fd_np(file.as_raw_fd(), ACL_TYPE_EXTENDED) };
if !acl.is_null() {
return Ok(Some(acl));
}
let error = io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::ENOENT) | Some(libc::ENOTSUP) | Some(libc::EOPNOTSUPP) => Ok(None),
_ => Err(error),
}
}
fn any_entry(acl: Acl, mut visit: impl FnMut(Entry) -> bool) -> bool {
let mut which = ACL_FIRST_ENTRY;
loop {
let mut entry: Entry = std::ptr::null_mut();
if unsafe { acl_get_entry(acl, which, &mut entry) } != 0 {
return false;
}
if visit(entry) {
return true;
}
which = ACL_NEXT_ENTRY;
}
}
fn is_allow(entry: Entry) -> bool {
let mut tag: c_int = 0;
if unsafe { acl_get_tag_type(entry, &mut tag) } != 0 {
return true;
}
tag != ACL_EXTENDED_DENY
}
pub fn has_allow_entry(file: &File) -> io::Result<bool> {
let Some(acl) = read(file)? else {
return Ok(false);
};
let any = any_entry(acl, is_allow);
unsafe { acl_free(acl) };
Ok(any)
}
pub fn path_has_allow_entry(path: &std::path::Path) -> io::Result<bool> {
use std::os::unix::ffi::OsStrExt;
let c_path = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains a NUL byte"))?;
let acl = unsafe { acl_get_link_np(c_path.as_ptr(), ACL_TYPE_EXTENDED) };
if acl.is_null() {
let error = io::Error::last_os_error();
return match error.raw_os_error() {
Some(libc::ENOENT) | Some(libc::ENOTSUP) | Some(libc::EOPNOTSUPP) => Ok(false),
_ => Err(error),
};
}
let any = any_entry(acl, is_allow);
unsafe { acl_free(acl) };
Ok(any)
}
pub fn dir_inherits_allow(dir: &File) -> io::Result<bool> {
let Some(acl) = read(dir)? else {
return Ok(false);
};
let found = any_entry(acl, |entry| {
if !is_allow(entry) {
return false;
}
let mut flags: Flagset = std::ptr::null_mut();
if unsafe { acl_get_flagset_np(entry, &mut flags) } != 0 {
return true;
}
let (file, directory) = unsafe {
(
acl_get_flag_np(flags, ACL_ENTRY_FILE_INHERIT),
acl_get_flag_np(flags, ACL_ENTRY_DIRECTORY_INHERIT),
)
};
file != 0 || directory != 0
});
unsafe { acl_free(acl) };
Ok(found)
}
}
#[cfg(not(target_os = "macos"))]
mod sys {
use std::fs::File;
use std::io;
pub fn has_allow_entry(_file: &File) -> io::Result<bool> {
Ok(false)
}
pub fn path_has_allow_entry(_path: &std::path::Path) -> io::Result<bool> {
Ok(false)
}
pub fn dir_inherits_allow(_dir: &File) -> io::Result<bool> {
Ok(false)
}
}
pub(crate) fn guard_parent(parent: &File, shown: &Path) -> Result<()> {
match sys::dir_inherits_allow(parent) {
Ok(false) => Ok(()),
Ok(true) => Err(FsError::UnsafeAcl(shown.to_path_buf())),
Err(e) => Err(FsError::io(shown, e)),
}
}
pub(crate) fn guard_parent_path(parent: &Path, shown: &Path) -> Result<()> {
if cfg!(not(target_os = "macos")) {
return Ok(());
}
match open_directory(parent) {
Ok(dir) => guard_parent(&dir, shown),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) if is_not_a_directory(&e) => Ok(()),
Err(e) => Err(FsError::io(parent, e)),
}
}
fn open_directory(path: &Path) -> std::io::Result<File> {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC)
.open(path)
}
#[cfg(not(unix))]
{
File::open(path)
}
}
fn is_not_a_directory(error: &std::io::Error) -> bool {
#[cfg(unix)]
{
error.raw_os_error() == Some(libc::ENOTDIR)
}
#[cfg(not(unix))]
{
let _ = error;
false
}
}
pub(crate) fn refuse_acl(file: &File, shown: &Path) -> Result<()> {
match sys::has_allow_entry(file) {
Ok(false) => Ok(()),
Ok(true) => Err(FsError::UnsafeAcl(shown.to_path_buf())),
Err(e) => Err(FsError::io(shown, e)),
}
}
pub(crate) fn refuse_acl_path(path: &Path) -> Result<()> {
if cfg!(not(target_os = "macos")) {
return Ok(());
}
let dir = open_directory(path).map_err(|e| FsError::io(path, e))?;
refuse_acl(&dir, path)
}
pub(crate) fn existing_has_allow_acl(path: &Path) -> Result<bool> {
sys::path_has_allow_entry(path).map_err(|e| FsError::io(path, e))
}