use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions};
use std::io::Write as _;
use std::os::unix::fs::{MetadataExt as _, OpenOptionsExt as _};
use std::path::{Component, Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Identity {
pub dev: u64,
pub ino: u64,
}
impl Identity {
pub fn of(metadata: &fs::Metadata) -> Self {
Self {
dev: metadata.dev(),
ino: metadata.ino(),
}
}
}
pub fn proc_path(dir: &File) -> PathBuf {
use std::os::fd::AsRawFd as _;
PathBuf::from(format!("/proc/self/fd/{}", dir.as_raw_fd()))
}
pub fn open_dir(path: &Path) -> std::io::Result<File> {
OpenOptions::new()
.read(true)
.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(path)
}
pub fn hold_dir(root: &File, relative: &Path) -> std::io::Result<File> {
hold_dir_with(root, relative, true)
}
pub fn hold_dir_existing(root: &File, relative: &Path) -> std::io::Result<File> {
hold_dir_with(root, relative, false)
}
fn hold_dir_with(root: &File, relative: &Path, create: bool) -> std::io::Result<File> {
let mut held = root.try_clone()?;
for component in relative.components() {
let name = match component {
Component::Normal(name) => name,
Component::CurDir => continue,
other => {
return Err(std::io::Error::other(format!(
"{} is not a plain component below the target",
other.as_os_str().to_string_lossy()
)));
}
};
let path = proc_path(&held).join(name);
let next = match open_dir(&path) {
Ok(dir) => dir,
Err(error) if create && error.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir(&path)?;
open_dir(&path)?
}
Err(error) => {
if fs::symlink_metadata(&path).is_ok_and(|entry| entry.file_type().is_symlink()) {
return Err(std::io::Error::other(format!(
"{} is a link, and a landing follows none",
name.to_string_lossy()
)));
}
return Err(error);
}
};
held = next;
}
Ok(held)
}
pub fn write_file(dir: &File, name: &OsStr, bytes: &[u8]) -> std::io::Result<()> {
let base = proc_path(dir);
let mut temp = std::ffi::OsString::from(format!(".{}.rk-tmp.", std::process::id()));
temp.push(name);
let temp_path = base.join(&temp);
let written = OpenOptions::new()
.write(true)
.create_new(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(&temp_path)
.and_then(|mut file| file.write_all(bytes).and_then(|()| file.sync_all()))
.and_then(|()| fs::rename(&temp_path, base.join(name)));
if written.is_err() {
let _ = fs::remove_file(&temp_path);
}
written
}
pub fn read_file(dir: &File, name: &OsStr) -> std::io::Result<Option<Vec<u8>>> {
use std::io::Read as _;
let path = proc_path(dir).join(name);
let opened = OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(&path);
match opened {
Ok(mut file) => {
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(Some(bytes))
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error),
}
}
pub fn nonce() -> u64 {
use std::io::Read as _;
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let mut bytes = [0u8; 8];
if File::open("/dev/urandom")
.and_then(|mut source| source.read_exact(&mut bytes))
.is_ok()
{
return u64::from_ne_bytes(bytes);
}
let since = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
(since.as_secs() << 30)
^ u64::from(since.subsec_nanos())
^ (u64::from(std::process::id()) << 48)
^ COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
pub fn quarantine_name(prefix: &str) -> std::ffi::OsString {
std::ffi::OsString::from(format!("{prefix}{:016x}", nonce()))
}
pub fn quarantine(
dir: &File,
name: &std::ffi::OsStr,
prefix: &str,
) -> std::io::Result<(std::ffi::OsString, fs::Metadata)> {
let base = proc_path(dir);
let quarantined = quarantine_name(prefix);
fs::rename(base.join(name), base.join(&quarantined))?;
let metadata = fs::symlink_metadata(base.join(&quarantined))?;
Ok((quarantined, metadata))
}
pub fn pause(var: &str, tag: &str, proceed: &str) {
let Some(dir) = std::env::var_os(var).filter(|value| !value.is_empty()) else {
return;
};
let dir = PathBuf::from(dir);
let _ = fs::write(dir.join(tag), b"");
let proceed = dir.join(proceed);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
while !proceed.exists() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
#[cfg(test)]
mod tests {
use super::{hold_dir, open_dir, read_file, write_file};
#[test]
fn a_hold_creates_and_refuses_a_linked_component() {
let root = tempfile::tempdir().expect("a scratch root exists");
let outside = tempfile::tempdir().expect("a scratch outside exists");
let held = open_dir(root.path()).expect("the root opens");
let leaf = hold_dir(&held, std::path::Path::new("a/b")).expect("the nested hold creates");
write_file(&leaf, std::ffi::OsStr::new("file.txt"), b"one").expect("the write lands");
assert_eq!(
std::fs::read(root.path().join("a/b/file.txt")).expect("reads"),
b"one"
);
assert_eq!(
read_file(&leaf, std::ffi::OsStr::new("file.txt")).expect("reads"),
Some(b"one".to_vec())
);
assert_eq!(
read_file(&leaf, std::ffi::OsStr::new("absent")).expect("reads"),
None
);
std::fs::remove_dir_all(root.path().join("a")).expect("the tree removes");
std::os::unix::fs::symlink(outside.path(), root.path().join("a")).expect("the link");
let refused = hold_dir(&held, std::path::Path::new("a/b")).expect_err("a link refuses");
assert!(refused.to_string().contains("link"), "{refused}");
assert_eq!(
std::fs::read_dir(outside.path()).expect("reads").count(),
0,
"nothing lands through the link"
);
}
}