use std::ffi::{CString, OsStr, OsString};
use std::fs::File;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt;
use std::path::{Component, Path, PathBuf};
const RESOLVE_NO_XDEV: u64 = 0x01;
const RESOLVE_NO_MAGICLINKS: u64 = 0x02;
const RESOLVE_NO_SYMLINKS: u64 = 0x04;
const RESOLVE_BENEATH: u64 = 0x08;
#[repr(C)]
struct OpenHow {
flags: u64,
mode: u64,
resolve: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ObjectId {
pub device: u64,
pub inode: u64,
pub birth: Option<(i64, u32)>,
}
impl ObjectId {
pub fn encode(&self) -> String {
return match self.birth {
Some((seconds, nanos)) => format!("{}:{}:{seconds}:{nanos}", self.device, self.inode),
None => format!("{}:{}:-:-", self.device, self.inode),
};
}
pub fn decode(text: &str) -> Option<ObjectId> {
let mut parts = text.split(':');
let device: u64 = parts.next()?.parse().ok()?;
let inode: u64 = parts.next()?.parse().ok()?;
let seconds: &str = parts.next()?;
let nanos: &str = parts.next()?;
let birth: Option<(i64, u32)> = if seconds == "-" { None } else { Some((seconds.parse().ok()?, nanos.parse().ok()?)) };
return if parts.next().is_none() { Some(ObjectId { device, inode, birth }) } else { None };
}
}
pub fn plain_components(relative: &Path) -> std::io::Result<Vec<OsString>> {
let mut names: Vec<OsString> = Vec::new();
for part in relative.components() {
match part {
Component::Normal(name) => names.push(name.to_os_string()),
_ => return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is not a plain relative path", relative.display()))),
}
}
return Ok(names);
}
fn name_of(name: &OsStr) -> std::io::Result<CString> {
let bytes: &[u8] = name.as_bytes();
if bytes.is_empty() || bytes == b"." || bytes == b".." || bytes.contains(&b'/') {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "a name must be one plain component"));
}
return CString::new(bytes).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a name holds a NUL byte"); });
}
fn checked(result: libc::c_int) -> std::io::Result<libc::c_int> {
return if result < 0 { Err(std::io::Error::last_os_error()) } else { Ok(result) };
}
fn open_beneath(dir: &File, name: &OsStr, flags: libc::c_int, mode: u32) -> std::io::Result<File> {
let c_name = name_of(name)?;
let how: OpenHow = OpenHow {
flags: (flags | libc::O_CLOEXEC) as u64,
mode: u64::from(mode),
resolve: RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS,
};
let result: libc::c_long = unsafe { libc::syscall(libc::SYS_openat2, dir.as_raw_fd(), c_name.as_ptr(), &how as *const OpenHow, std::mem::size_of::<OpenHow>()) };
if result < 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(File::from(unsafe { OwnedFd::from_raw_fd(result as libc::c_int) }));
}
pub fn open_root(path: &Path) -> std::io::Result<File> {
if !path.is_absolute() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is not absolute", path.display())));
}
let name = CString::new(path.as_os_str().as_bytes()).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a path holds a NUL byte"); })?;
let descriptor: libc::c_int = checked(unsafe { libc::open(name.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC) })?;
let held: File = File::from(unsafe { OwnedFd::from_raw_fd(descriptor) });
let real: PathBuf = std::fs::read_link(handle_path(&held))?;
if real.as_os_str() != path.as_os_str() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is reached through a link: it is {}", path.display(), real.display())));
}
return Ok(held);
}
pub fn open_directory_at(parent: &File, name: &OsStr) -> std::io::Result<File> {
return open_beneath(parent, name, libc::O_RDONLY | libc::O_DIRECTORY, 0);
}
pub fn open_directory_beneath(root: &File, names: &[OsString]) -> std::io::Result<File> {
let device: u64 = device_of(root)?;
let mut current: File = root.try_clone()?;
for name in names {
let next: File = open_directory_at(¤t, name.as_os_str())?;
if device_of(&next)? != device {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} is on another device", name.to_string_lossy())));
}
current = next;
}
return Ok(current);
}
pub fn device_of(file: &File) -> std::io::Result<u64> {
use std::os::unix::fs::MetadataExt;
return Ok(file.metadata()?.dev());
}
pub fn open_file_at(dir: &File, name: &OsStr) -> std::io::Result<File> {
return open_beneath(dir, name, libc::O_RDONLY | libc::O_NONBLOCK, 0);
}
pub fn create_file_at(dir: &File, name: &OsStr, mode: u32) -> std::io::Result<File> {
return open_beneath(dir, name, libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL, mode);
}
pub fn make_directory_at(dir: &File, name: &OsStr, mode: u32) -> std::io::Result<bool> {
let c_name = name_of(name)?;
let result: libc::c_int = unsafe { libc::mkdirat(dir.as_raw_fd(), c_name.as_ptr(), mode as libc::mode_t) };
if result == 0 {
return Ok(true);
}
let error: std::io::Error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::AlreadyExists {
drop(open_directory_at(dir, name)?);
return Ok(false);
}
return Err(error);
}
pub fn symlink_at(target: &Path, dir: &File, name: &OsStr) -> std::io::Result<()> {
let c_name = name_of(name)?;
let c_target = CString::new(target.as_os_str().as_bytes()).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a link target holds a NUL byte"); })?;
checked(unsafe { libc::symlinkat(c_target.as_ptr(), dir.as_raw_fd(), c_name.as_ptr()) })?;
return Ok(());
}
pub fn read_link_at(dir: &File, name: &OsStr) -> std::io::Result<PathBuf> {
use std::os::unix::ffi::OsStringExt;
let c_name = name_of(name)?;
let mut buffer: Vec<u8> = vec![0; 4096];
let length: libc::ssize_t = unsafe { libc::readlinkat(dir.as_raw_fd(), c_name.as_ptr(), buffer.as_mut_ptr().cast::<libc::c_char>(), buffer.len()) };
if length < 0 {
return Err(std::io::Error::last_os_error());
}
buffer.truncate(usize::try_from(length).unwrap_or(0));
return Ok(PathBuf::from(OsString::from_vec(buffer)));
}
pub fn handle_path(directory: &File) -> PathBuf {
return Path::new("/proc/self/fd").join(directory.as_raw_fd().to_string());
}
pub fn entry_metadata_at(dir: &File, name: &OsStr) -> std::io::Result<std::fs::Metadata> {
name_of(name)?;
return std::fs::symlink_metadata(handle_path(dir).join(name));
}
pub fn set_times_at(dir: &File, name: &OsStr, times: [libc::timespec; 2]) -> std::io::Result<()> {
let c_name = name_of(name)?;
checked(unsafe { libc::utimensat(dir.as_raw_fd(), c_name.as_ptr(), times.as_ptr(), libc::AT_SYMLINK_NOFOLLOW) })?;
return Ok(());
}
pub fn remove_at(dir: &File, name: &OsStr, directory: bool) -> std::io::Result<()> {
let c_name = name_of(name)?;
let flags: libc::c_int = if directory { libc::AT_REMOVEDIR } else { 0 };
checked(unsafe { libc::unlinkat(dir.as_raw_fd(), c_name.as_ptr(), flags) })?;
return Ok(());
}
pub fn rename_at(from_dir: &File, from: &OsStr, to_dir: &File, to: &OsStr, replace: bool) -> std::io::Result<()> {
let c_from = name_of(from)?;
let c_to = name_of(to)?;
let flags: libc::c_uint = if replace { 0 } else { libc::RENAME_NOREPLACE };
checked(unsafe { libc::renameat2(from_dir.as_raw_fd(), c_from.as_ptr(), to_dir.as_raw_fd(), c_to.as_ptr(), flags) })?;
return Ok(());
}
pub fn exchange_at(dir: &File, first: &OsStr, second: &OsStr) -> std::io::Result<()> {
let c_first = name_of(first)?;
let c_second = name_of(second)?;
checked(unsafe { libc::renameat2(dir.as_raw_fd(), c_first.as_ptr(), dir.as_raw_fd(), c_second.as_ptr(), libc::RENAME_EXCHANGE) })?;
return Ok(());
}
fn id_of_statx(found: &libc::statx) -> ObjectId {
let birth: Option<(i64, u32)> = if found.stx_mask & libc::STATX_BTIME != 0 { Some((found.stx_btime.tv_sec, found.stx_btime.tv_nsec)) } else { None };
return ObjectId { device: libc::makedev(found.stx_dev_major, found.stx_dev_minor), inode: found.stx_ino, birth };
}
fn identity_statx(dir: &File, name: Option<&OsStr>) -> std::io::Result<ObjectId> {
let (c_name, flags): (CString, libc::c_int) = match name {
Some(plain) => (name_of(plain)?, libc::AT_SYMLINK_NOFOLLOW),
None => (CString::default(), libc::AT_EMPTY_PATH),
};
let mut found: libc::statx = unsafe { std::mem::zeroed() };
checked(unsafe { libc::statx(dir.as_raw_fd(), c_name.as_ptr(), flags, libc::STATX_BASIC_STATS | libc::STATX_BTIME, &mut found) })?;
return Ok(id_of_statx(&found));
}
pub fn identity_of(file: &File) -> std::io::Result<ObjectId> {
return identity_statx(file, None);
}
pub fn identity_at(dir: &File, name: &OsStr) -> std::io::Result<ObjectId> {
return identity_statx(dir, Some(name));
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Owned {
Removed,
Absent,
Kept(String),
}
pub fn remove_if_identical(dir: &File, name: &OsStr, expected: ObjectId) -> std::io::Result<Owned> {
let found: ObjectId = match identity_at(dir, name) {
Ok(known) => known,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Owned::Absent),
Err(error) => return Err(error),
};
if found != expected {
return Ok(Owned::Kept(format!("{} is not the object that was made ({} against {})", name.to_string_lossy(), found.encode(), expected.encode())));
}
if entry_metadata_at(dir, name)?.is_dir() {
return Ok(Owned::Kept(format!("{} is a directory and is not removed", name.to_string_lossy())));
}
return match remove_at(dir, name, false) {
Ok(()) => Ok(Owned::Removed),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Owned::Absent),
Err(error) => Err(error),
};
}
pub fn link_open_file_at(source: &File, dir: &File, name: &OsStr) -> std::io::Result<()> {
let c_name = name_of(name)?;
let through = CString::new(handle_path(source).as_os_str().as_bytes()).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, "a path holds a NUL byte"); })?;
checked(unsafe { libc::linkat(libc::AT_FDCWD, through.as_ptr(), dir.as_raw_fd(), c_name.as_ptr(), libc::AT_SYMLINK_FOLLOW) })?;
return Ok(());
}
pub fn widen_directory(directory: &File) -> std::io::Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let mode: u32 = directory.metadata()?.mode();
if mode & 0o700 == 0o700 {
return Ok(());
}
return directory.set_permissions(std::fs::Permissions::from_mode(mode | 0o700));
}
pub fn open_directory_widening(parent: &File, name: &OsStr) -> std::io::Result<File> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
match open_directory_at(parent, name) {
Err(error) if error.raw_os_error() == Some(libc::EACCES) => {
let handle: File = open_beneath(parent, name, libc::O_PATH | libc::O_DIRECTORY, 0)?;
let mode: u32 = handle.metadata()?.mode();
if std::fs::set_permissions(handle_path(&handle), std::fs::Permissions::from_mode(mode | 0o700)).is_err() {
return Err(error);
}
return open_directory_at(parent, name);
}
other => return other,
}
}
#[cfg(test)]
#[path = "fs_beneath_tests.rs"]
mod tests;