use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fs::{self, File, Metadata};
use std::io::{self, BufRead, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
pub(crate) const CHUNK: usize = 5000;
pub(crate) const SANDBOX_OUTPUT_CAP: u64 = 8 * 1024 * 1024;
pub(crate) fn reset_sigpipe() {
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
#[cfg(unix)]
const CLEANUP_SIGNALS: [libc::c_int; 6] = [
libc::SIGINT,
libc::SIGTERM,
libc::SIGHUP,
libc::SIGPIPE,
libc::SIGXCPU,
libc::SIGXFSZ,
];
#[cfg(unix)]
const SLOT_LEN: usize = libc::PATH_MAX as usize;
#[cfg(unix)]
struct PathSlot(std::cell::UnsafeCell<[u8; SLOT_LEN]>);
#[cfg(unix)]
unsafe impl Sync for PathSlot {}
#[cfg(unix)]
static SLOT: PathSlot = PathSlot(std::cell::UnsafeCell::new([0; SLOT_LEN]));
#[cfg(unix)]
static ARMED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[must_use = "dropping the guard disarms the cleanup at once"]
pub(crate) struct CleanupGuard {
#[cfg(unix)]
armed: bool,
}
impl Drop for CleanupGuard {
fn drop(&mut self) {
#[cfg(unix)]
if self.armed {
ARMED.store(false, Ordering::Release);
}
}
}
pub(crate) fn arm_cleanup(host: &Host, path: &Path) -> CleanupGuard {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
let bytes = path.as_os_str().as_bytes();
if matches!(host, Host::Sandbox(_)) || bytes.len() >= SLOT_LEN || bytes.contains(&0) {
return CleanupGuard { armed: false };
}
debug_assert!(
!ARMED.load(Ordering::Acquire),
"arm_cleanup: another output is still armed"
);
install_handlers();
unsafe {
let mut block: libc::sigset_t = std::mem::zeroed();
let mut old: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut block);
for sig in CLEANUP_SIGNALS {
libc::sigaddset(&mut block, sig);
}
libc::pthread_sigmask(libc::SIG_BLOCK, &block, &mut old);
ARMED.store(false, Ordering::SeqCst);
let slot = SLOT.0.get().cast::<u8>();
std::ptr::copy_nonoverlapping(bytes.as_ptr(), slot, bytes.len());
slot.add(bytes.len()).write(0);
ARMED.store(true, Ordering::Release);
libc::pthread_sigmask(libc::SIG_SETMASK, &old, std::ptr::null_mut());
}
CleanupGuard { armed: true }
}
#[cfg(not(unix))]
{
let _ = (host, path);
CleanupGuard {}
}
}
#[cfg(unix)]
fn install_handlers() {
let handler = on_cleanup_signal as extern "C" fn(libc::c_int) as libc::sighandler_t;
unsafe {
let mut action: libc::sigaction = std::mem::zeroed();
libc::sigemptyset(&mut action.sa_mask);
for sig in CLEANUP_SIGNALS {
libc::sigaddset(&mut action.sa_mask, sig);
}
action.sa_flags = 0;
action.sa_sigaction = handler;
for sig in CLEANUP_SIGNALS {
let mut old: libc::sigaction = std::mem::zeroed();
if libc::sigaction(sig, std::ptr::null(), &mut old) == 0
&& (old.sa_sigaction == libc::SIG_IGN || old.sa_sigaction == handler)
{
continue;
}
libc::sigaction(sig, &action, std::ptr::null_mut());
}
}
}
#[cfg(unix)]
extern "C" fn on_cleanup_signal(sig: libc::c_int) {
if ARMED.load(Ordering::Acquire) {
unsafe { libc::unlink(SLOT.0.get().cast::<libc::c_char>()) };
}
unsafe {
libc::signal(sig, libc::SIG_DFL);
libc::raise(sig);
}
}
#[cfg(unix)]
const PROGRESS_SIGNALS: &[libc::c_int] = &[
libc::SIGALRM,
#[cfg(any(
target_vendor = "apple",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
))]
libc::SIGINFO,
libc::SIGUSR1,
];
pub(crate) static PROGRESS_REQUESTED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub(crate) fn install_progress_signals(host: &Host) {
#[cfg(unix)]
if matches!(host, Host::Real) {
let handler = on_progress_signal as extern "C" fn(libc::c_int) as libc::sighandler_t;
unsafe {
let mut action: libc::sigaction = std::mem::zeroed();
libc::sigemptyset(&mut action.sa_mask);
action.sa_flags = libc::SA_RESTART;
action.sa_sigaction = handler;
for &sig in PROGRESS_SIGNALS {
libc::sigaction(sig, &action, std::ptr::null_mut());
}
}
}
#[cfg(not(unix))]
let _ = host;
}
#[cfg(unix)]
extern "C" fn on_progress_signal(_sig: libc::c_int) {
PROGRESS_REQUESTED.store(true, Ordering::Relaxed);
}
static STOPPED_MS: AtomicU64 = AtomicU64::new(0);
fn monotonic_ms() -> u64 {
#[cfg(unix)]
{
let mut ts: libc::timespec = unsafe { std::mem::zeroed() };
unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
(ts.tv_sec as u64).wrapping_mul(1000) + (ts.tv_nsec as u64) / 1_000_000
}
#[cfg(not(unix))]
{
static BASE: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
BASE.get_or_init(std::time::Instant::now)
.elapsed()
.as_millis() as u64
}
}
pub(crate) fn running_ms() -> u64 {
monotonic_ms().wrapping_sub(STOPPED_MS.load(Ordering::Relaxed))
}
pub(crate) fn install_stop_handler(host: &Host) {
#[cfg(unix)]
if matches!(host, Host::Real) {
let handler = on_stop_signal as extern "C" fn(libc::c_int) as libc::sighandler_t;
unsafe {
let mut old: libc::sigaction = std::mem::zeroed();
if libc::sigaction(libc::SIGTSTP, std::ptr::null(), &mut old) == 0
&& old.sa_sigaction == libc::SIG_IGN
{
return;
}
let mut action: libc::sigaction = std::mem::zeroed();
libc::sigemptyset(&mut action.sa_mask);
for &sig in CLEANUP_SIGNALS.iter().chain(PROGRESS_SIGNALS) {
libc::sigaddset(&mut action.sa_mask, sig);
}
libc::sigaddset(&mut action.sa_mask, libc::SIGTSTP);
action.sa_flags = 0;
action.sa_sigaction = handler;
libc::sigaction(libc::SIGTSTP, &action, std::ptr::null_mut());
}
}
#[cfg(not(unix))]
let _ = host;
}
#[cfg(unix)]
extern "C" fn on_stop_signal(_sig: libc::c_int) {
let t = monotonic_ms();
unsafe { libc::raise(libc::SIGSTOP) };
STOPPED_MS.fetch_add(monotonic_ms().wrapping_sub(t), Ordering::Relaxed);
}
pub(crate) fn create_temp(dir: &Path, family: &str) -> io::Result<(PathBuf, File)> {
let mut opts = File::options();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
for n in 0u32.. {
let tmp = dir.join(format!(".stuffr-{family}-{}-{n}.tmp", std::process::id()));
match opts.open(&tmp) {
Ok(f) => return Ok((tmp, f)),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists && n < 1000 => {}
Err(e) => return Err(e),
}
}
unreachable!("the loop returns")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Published {
Done,
Exists,
}
pub(crate) fn publish(tmp: &Path, out: &Path, force: bool) -> io::Result<Published> {
if force {
fs::rename(tmp, out)?;
return Ok(Published::Done);
}
match fs::hard_link(tmp, out) {
Ok(()) => fs::remove_file(tmp)?,
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
let _ = fs::remove_file(tmp);
return Ok(Published::Exists);
}
Err(_) => fs::rename(tmp, out)?,
}
Ok(Published::Done)
}
pub(crate) fn path(b: &[u8]) -> PathBuf {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
PathBuf::from(OsString::from_vec(b.to_vec()))
}
#[cfg(not(unix))]
{
PathBuf::from(String::from_utf8_lossy(b).into_owned())
}
}
pub(crate) fn strerror(e: &io::Error) -> Vec<u8> {
match e.raw_os_error() {
Some(n) => errno_text(n),
None => e.to_string().into_bytes(),
}
}
pub(crate) fn errno_text(n: i32) -> Vec<u8> {
let s = io::Error::from_raw_os_error(n).to_string();
let suffix = format!(" (os error {n})");
s.strip_suffix(&suffix).unwrap_or(&s).as_bytes().to_vec()
}
pub(crate) fn not_a_standard_file(p: &Path) -> bool {
fs::symlink_metadata(p).map_or(true, |m| !m.file_type().is_file())
}
pub(crate) fn same_file(a: &Metadata, b: &Metadata) -> Option<bool> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
Some(a.dev() == b.dev() && a.ino() == b.ino())
}
#[cfg(not(unix))]
{
let _ = (a, b);
None
}
}
pub(crate) fn count_hard_links(p: &Path) -> u64 {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
fs::symlink_metadata(p).map_or(0, |m| m.nlink().saturating_sub(1))
}
#[cfg(not(unix))]
{
let _ = p;
0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Kind {
Regular,
Device,
Pipe,
}
pub(crate) fn kind_of(f: &File) -> Kind {
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
match f.metadata().map(|m| m.file_type()) {
Ok(t) if t.is_fifo() || t.is_socket() => Kind::Pipe,
Ok(t) if t.is_char_device() || t.is_block_device() => Kind::Device,
_ => Kind::Regular,
}
}
#[cfg(not(unix))]
{
let _ = f;
Kind::Regular
}
}
pub(crate) fn stdin_file() -> io::Result<File> {
#[cfg(unix)]
{
use std::os::fd::AsFd;
Ok(File::from(io::stdin().as_fd().try_clone_to_owned()?))
}
#[cfg(not(unix))]
{
Err(io::Error::from(io::ErrorKind::Unsupported))
}
}
pub(crate) fn stdout_writer() -> Box<dyn Write + Send> {
#[cfg(unix)]
{
use std::os::fd::AsFd;
if let Ok(fd) = io::stdout().as_fd().try_clone_to_owned() {
return Box::new(File::from(fd));
}
}
Box::new(io::stdout())
}
#[doc(hidden)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct InputReads {
pub opens: u64,
pub bytes: u64,
pub len: u64,
}
pub(crate) type ReadLog = Arc<Mutex<BTreeMap<PathBuf, InputReads>>>;
#[derive(Debug)]
pub(crate) struct InFile {
file: File,
log: Option<(ReadLog, PathBuf)>,
}
impl InFile {
pub(crate) fn try_clone(&self) -> io::Result<InFile> {
Ok(InFile {
file: self.file.try_clone()?,
log: self.log.clone(),
})
}
}
impl From<File> for InFile {
fn from(file: File) -> Self {
InFile { file, log: None }
}
}
impl std::ops::Deref for InFile {
type Target = File;
fn deref(&self) -> &File {
&self.file
}
}
impl Read for InFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.file.read(buf)?;
if let Some((log, path)) = &self.log {
let mut log = log.lock().unwrap_or_else(|p| p.into_inner());
log.entry(path.clone()).or_default().bytes += n as u64;
}
Ok(n)
}
}
impl Seek for InFile {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.file.seek(pos)
}
}
pub(crate) struct Input {
src: InputSrc,
buf: Box<[u8; CHUNK]>,
pos: usize,
len: usize,
eof: bool,
pub(crate) kind: Kind,
}
pub(crate) enum InputSrc {
File(InFile),
Stdin(io::Stdin),
Bytes(io::Cursor<Vec<u8>>),
}
impl Read for InputSrc {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
InputSrc::File(f) => f.read(buf),
InputSrc::Stdin(s) => s.read(buf),
InputSrc::Bytes(c) => c.read(buf),
}
}
}
impl Input {
pub(crate) fn file(f: impl Into<InFile>) -> Self {
let f = f.into();
let kind = kind_of(&f);
Self::new(InputSrc::File(f), kind)
}
pub(crate) fn stdin() -> Self {
match stdin_file() {
Ok(f) => Self::file(f),
Err(_) => Self::new(InputSrc::Stdin(io::stdin()), Kind::Pipe),
}
}
pub(crate) fn bytes(data: Vec<u8>) -> Self {
Self::new(InputSrc::Bytes(io::Cursor::new(data)), Kind::Pipe)
}
fn new(src: InputSrc, kind: Kind) -> Self {
Self {
src,
buf: Box::new([0; CHUNK]),
pos: 0,
len: 0,
eof: false,
kind,
}
}
fn read_src(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.eof {
return Ok(0);
}
loop {
match self.src.read(buf) {
Ok(0) => {
self.eof = true;
return Ok(0);
}
Ok(n) => return Ok(n),
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
}
pub(crate) fn copy_raw(&mut self, out: &mut dyn Write) -> io::Result<()> {
if let InputSrc::File(f) = &mut self.src
&& f.seek(SeekFrom::Start(0)).is_ok()
{
self.eof = false;
}
self.pos = 0;
self.len = 0;
let mut buf = [0u8; CHUNK];
loop {
let n = self.read_src(&mut buf)?;
if n == 0 {
return Ok(());
}
out.write_all(&buf[..n])?;
}
}
}
impl Read for Input {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
let avail = self.fill_buf()?;
let n = avail.len().min(out.len());
out[..n].copy_from_slice(&avail[..n]);
self.consume(n);
Ok(n)
}
}
impl BufRead for Input {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
if self.pos == self.len {
let mut n = 0;
let mut chunk = [0u8; CHUNK];
while n < CHUNK {
match self.read_src(&mut chunk[n..])? {
0 => break,
k => n += k,
}
}
self.buf[..n].copy_from_slice(&chunk[..n]);
self.pos = 0;
self.len = n;
}
Ok(&self.buf[self.pos..self.len])
}
fn consume(&mut self, n: usize) {
self.pos = (self.pos + n).min(self.len);
}
}
pub(crate) struct Counting<W> {
pub(crate) inner: W,
pub(crate) n: Arc<AtomicU64>,
}
impl<W: Write> Write for Counting<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let k = self.inner.write(buf)?;
self.n.fetch_add(k as u64, Ordering::Relaxed);
Ok(k)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
pub(crate) enum Host {
Real,
Sandbox(Sandbox),
}
pub(crate) struct Sandbox {
pub(crate) dir: PathBuf,
pub(crate) stdin: Option<Vec<u8>>,
pub(crate) stdout: Arc<Mutex<Vec<u8>>>,
pub(crate) budget: Arc<AtomicU64>,
pub(crate) reads: ReadLog,
}
impl Host {
pub(crate) fn path(&self, b: &[u8]) -> PathBuf {
match self {
Host::Real => path(b),
Host::Sandbox(s) => s.dir.join(path(b)),
}
}
pub(crate) fn track(&self, path: &Path, f: File) -> InFile {
match self {
Host::Real => InFile::from(f),
Host::Sandbox(s) => {
let len = f.metadata().map_or(0, |m| m.len());
let mut log = s.reads.lock().unwrap_or_else(|p| p.into_inner());
let entry = log.entry(path.to_path_buf()).or_default();
entry.opens += 1;
entry.len = entry.len.max(len);
InFile {
file: f,
log: Some((s.reads.clone(), path.to_path_buf())),
}
}
}
}
pub(crate) fn stdin_is_tty(&self) -> bool {
matches!(self, Host::Real) && crate::compat::stdin_is_tty()
}
pub(crate) fn stdout_is_tty(&self) -> bool {
matches!(self, Host::Real) && crate::compat::stdout_is_tty()
}
pub(crate) fn stderr_is_tty(&self) -> bool {
use std::io::IsTerminal;
matches!(self, Host::Real) && io::stderr().is_terminal()
}
pub(crate) fn stdin(&mut self) -> Input {
match self {
Host::Real => Input::stdin(),
Host::Sandbox(s) => Input::bytes(s.stdin.take().unwrap_or_default()),
}
}
pub(crate) fn charge(&self, n: u64) -> io::Result<()> {
match self {
Host::Real => Ok(()),
Host::Sandbox(s) => take(&s.budget, n),
}
}
pub(crate) fn out_writer(&self, f: Option<File>) -> BufWriter<Box<dyn Write + Send>> {
let w: Box<dyn Write + Send> = match (self, f) {
(Host::Real, Some(f)) => Box::new(f),
(Host::Real, None) => stdout_writer(),
(Host::Sandbox(s), f) => {
let inner: Box<dyn Write + Send> = match f {
Some(f) => Box::new(f),
None => Box::new(Captured(s.stdout.clone())),
};
Box::new(Capped {
inner,
budget: s.budget.clone(),
})
}
};
BufWriter::with_capacity(64 * 1024, w)
}
}
struct Captured(Arc<Mutex<Vec<u8>>>);
impl Write for Captured {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut v = self.0.lock().unwrap_or_else(|p| p.into_inner());
v.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
struct Capped {
inner: Box<dyn Write + Send>,
budget: Arc<AtomicU64>,
}
impl Write for Capped {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let want = buf.len() as u64;
take(&self.budget, want)?;
let n = match self.inner.write(buf) {
Ok(n) => n,
Err(e) => {
self.budget.fetch_add(want, Ordering::Relaxed);
return Err(e);
}
};
self.budget.fetch_add(want - n as u64, Ordering::Relaxed);
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
fn take(budget: &AtomicU64, n: u64) -> io::Result<()> {
let mut left = budget.load(Ordering::Relaxed);
loop {
let rest = left.checked_sub(n).ok_or_else(no_space)?;
match budget.compare_exchange_weak(left, rest, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => return Ok(()),
Err(now) => left = now,
}
}
}
fn no_space() -> io::Error {
#[cfg(unix)]
{
io::Error::from_raw_os_error(libc::ENOSPC)
}
#[cfg(not(unix))]
{
io::Error::from(io::ErrorKind::StorageFull)
}
}
pub(crate) fn printf(w: &mut dyn Write, fmt: &str, args: &[&[u8]]) {
let mut out = Vec::with_capacity(fmt.len() + 64);
let mut args = args.iter();
let mut bytes = fmt.as_bytes().iter().copied();
while let Some(b) = bytes.next() {
if b != b'%' {
out.push(b);
continue;
}
match bytes.next() {
Some(b'%') => out.push(b'%'),
Some(b's' | b'd') => out.extend_from_slice(args.next().copied().unwrap_or_default()),
Some(other) => out.extend_from_slice(&[b'%', other]),
None => out.push(b'%'),
}
}
let _ = w.write_all(&out);
}
pub(crate) fn set_times(meta: &Metadata, to: &Path) -> io::Result<()> {
let mut times = fs::FileTimes::new();
if let Ok(t) = meta.accessed() {
times = times.set_accessed(t);
}
if let Ok(t) = meta.modified() {
times = times.set_modified(t);
}
File::options().write(true).open(to)?.set_times(times)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_capped_writer_never_wraps_its_budget() {
let budget = Arc::new(AtomicU64::new(10));
let mut w = Capped {
inner: Box::new(io::sink()),
budget: budget.clone(),
};
assert_eq!(w.write(b"1234567").unwrap(), 7);
assert_eq!(budget.load(Ordering::Relaxed), 3);
let e = w.write(b"1234567").unwrap_err();
assert_eq!(e.kind(), no_space().kind());
assert_eq!(budget.load(Ordering::Relaxed), 3);
assert_eq!(w.write(b"123").unwrap(), 3);
assert_eq!(budget.load(Ordering::Relaxed), 0);
assert!(w.write(b"x").is_err());
assert_eq!(w.write(b"").unwrap(), 0);
}
fn scratch(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("stuffr-host-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn publish_never_replaces_an_output_that_appeared_meanwhile() {
let dir = scratch("exists");
let (tmp, out) = (dir.join(".tmp"), dir.join("a.out"));
fs::write(&tmp, b"new").unwrap();
fs::write(&out, b"appeared").unwrap();
assert_eq!(publish(&tmp, &out, false).unwrap(), Published::Exists);
assert!(!tmp.exists(), "the temporary is left");
assert_eq!(fs::read(&out).unwrap(), b"appeared");
fs::write(&tmp, b"new").unwrap();
fs::remove_file(&out).unwrap();
assert_eq!(publish(&tmp, &out, false).unwrap(), Published::Done);
assert!(!tmp.exists());
assert_eq!(fs::read(&out).unwrap(), b"new");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn publish_with_force_replaces() {
let dir = scratch("force");
let (tmp, out) = (dir.join(".tmp"), dir.join("a.out"));
fs::write(&tmp, b"new").unwrap();
fs::write(&out, b"old").unwrap();
assert_eq!(publish(&tmp, &out, true).unwrap(), Published::Done);
assert!(!tmp.exists());
assert_eq!(fs::read(&out).unwrap(), b"new");
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn create_temp_names_the_family_and_is_0600() {
use std::os::unix::fs::PermissionsExt;
let dir = scratch("temp");
let (a, _fa) = create_temp(&dir, "gzip").unwrap();
let (b, _fb) = create_temp(&dir, "gzip").unwrap();
assert_ne!(a, b);
let name = a.file_name().unwrap().to_str().unwrap().to_owned();
assert!(
name.starts_with(&format!(".stuffr-gzip-{}-", std::process::id()))
&& name.ends_with(".tmp"),
"{name}"
);
assert_eq!(
fs::metadata(&a).unwrap().permissions().mode() & 0o777,
0o600
);
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn arm_then_drop_disarms() {
use std::os::unix::ffi::OsStrExt;
let dir = scratch("arm");
let file = dir.join("partial.xz");
fs::write(&file, b"partial").unwrap();
let bytes = file.as_os_str().as_bytes();
{
let _guard = arm_cleanup(&Host::Real, &file);
assert!(ARMED.load(Ordering::Acquire), "not armed");
let slot = unsafe { &*SLOT.0.get() };
assert_eq!(&slot[..bytes.len()], bytes);
assert_eq!(slot[bytes.len()], 0);
}
assert!(!ARMED.load(Ordering::Acquire), "still armed after drop");
let long = dir.join("x".repeat(SLOT_LEN));
let guard = arm_cleanup(&Host::Real, &long);
assert!(!ARMED.load(Ordering::Acquire), "an over-long path armed");
drop(guard);
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
fn in_child(child: impl FnOnce()) -> libc::c_int {
unsafe {
let pid = libc::fork();
assert!(pid >= 0, "fork: {}", io::Error::last_os_error());
if pid == 0 {
child();
libc::_exit(99);
}
let mut status = 0;
while libc::waitpid(pid, &mut status, 0) < 0 {
assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EINTR));
}
status
}
}
#[cfg(unix)]
fn default_dispositions() {
for sig in CLEANUP_SIGNALS {
unsafe { libc::signal(sig, libc::SIG_DFL) };
}
}
#[cfg(unix)]
#[test]
fn an_interrupt_removes_the_armed_file() {
let dir = scratch("interrupt");
for sig in CLEANUP_SIGNALS {
let file = dir.join(format!("partial-{sig}.xz"));
fs::write(&file, b"partial").unwrap();
let status = in_child(|| {
default_dispositions();
ARMED.store(false, Ordering::SeqCst);
let _guard = arm_cleanup(&Host::Real, &file);
unsafe { libc::raise(sig) };
});
assert!(
libc::WIFSIGNALED(status),
"signal {sig}: the child exited (status {status:#x})"
);
assert_eq!(libc::WTERMSIG(status), sig);
assert!(!file.exists(), "signal {sig}: the partial file survived");
}
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn sandbox_never_installs_handlers() {
let dir = scratch("sandbox-signals");
let file = dir.join("partial.xz");
fs::write(&file, b"partial").unwrap();
let host = Host::Sandbox(Sandbox {
dir: dir.clone(),
stdin: None,
stdout: Arc::default(),
budget: Arc::new(AtomicU64::new(0)),
reads: ReadLog::default(),
});
let status = in_child(|| {
default_dispositions();
ARMED.store(false, Ordering::SeqCst);
let _guard = arm_cleanup(&host, &file);
let mut all_default = !ARMED.load(Ordering::Acquire);
for sig in CLEANUP_SIGNALS {
unsafe {
let mut old: libc::sigaction = std::mem::zeroed();
let queried = libc::sigaction(sig, std::ptr::null(), &mut old) == 0;
all_default &= queried && old.sa_sigaction == libc::SIG_DFL;
}
}
unsafe { libc::_exit(if all_default { 0 } else { 1 }) };
});
assert!(
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0,
"a sandboxed arm installed a handler (status {status:#x})"
);
assert!(file.exists());
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn progress_signals_only_set_the_flag() {
for &sig in PROGRESS_SIGNALS {
let status = in_child(|| {
unsafe { libc::signal(sig, libc::SIG_IGN) };
PROGRESS_REQUESTED.store(false, Ordering::SeqCst);
install_progress_signals(&Host::Real);
unsafe { libc::raise(sig) };
let set = PROGRESS_REQUESTED.load(Ordering::SeqCst);
unsafe { libc::_exit(if set { 0 } else { 1 }) };
});
assert!(
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0,
"signal {sig}: status {status:#x}"
);
}
assert!(PROGRESS_SIGNALS.contains(&libc::SIGUSR1));
#[cfg(target_vendor = "apple")]
assert!(PROGRESS_SIGNALS.contains(&libc::SIGINFO));
let host = Host::Sandbox(Sandbox {
dir: std::env::temp_dir(),
stdin: None,
stdout: Arc::default(),
budget: Arc::new(AtomicU64::new(0)),
reads: ReadLog::default(),
});
let status = in_child(|| {
let mut all_default = true;
for &sig in PROGRESS_SIGNALS.iter().chain(&[libc::SIGTSTP]) {
unsafe { libc::signal(sig, libc::SIG_DFL) };
}
install_progress_signals(&host);
install_stop_handler(&host);
for &sig in PROGRESS_SIGNALS.iter().chain(&[libc::SIGTSTP]) {
unsafe {
let mut old: libc::sigaction = std::mem::zeroed();
let queried = libc::sigaction(sig, std::ptr::null(), &mut old) == 0;
all_default &= queried && old.sa_sigaction == libc::SIG_DFL;
}
}
unsafe { libc::_exit(if all_default { 0 } else { 1 }) };
});
assert!(
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0,
"a sandbox installed a progress handler (status {status:#x})"
);
}
#[cfg(unix)]
#[test]
fn a_stop_is_left_out_of_the_running_clock() {
unsafe {
let pid = libc::fork();
assert!(pid >= 0, "fork: {}", io::Error::last_os_error());
if pid == 0 {
libc::signal(libc::SIGTSTP, libc::SIG_DFL);
install_stop_handler(&Host::Real);
let before = STOPPED_MS.load(Ordering::SeqCst);
libc::raise(libc::SIGTSTP);
let stopped = STOPPED_MS.load(Ordering::SeqCst) - before;
libc::_exit(if stopped >= 150 { 0 } else { 1 });
}
let mut status = 0;
while libc::waitpid(pid, &mut status, libc::WUNTRACED) < 0 {}
assert!(libc::WIFSTOPPED(status), "status {status:#x}");
assert_eq!(libc::WSTOPSIG(status), libc::SIGSTOP);
std::thread::sleep(std::time::Duration::from_millis(250));
libc::kill(pid, libc::SIGCONT);
while libc::waitpid(pid, &mut status, 0) < 0 {}
assert!(
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0,
"the stop was not counted (status {status:#x})"
);
}
let status = in_child(|| {
unsafe {
libc::signal(libc::SIGTSTP, libc::SIG_IGN);
install_stop_handler(&Host::Real);
let mut old: libc::sigaction = std::mem::zeroed();
libc::sigaction(libc::SIGTSTP, std::ptr::null(), &mut old);
libc::_exit(if old.sa_sigaction == libc::SIG_IGN {
0
} else {
1
});
}
});
assert!(
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0,
"an ignored SIGTSTP was hooked (status {status:#x})"
);
}
}