landstrip 0.12.3

Sandbox for coding agents with parametrized state
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2026 Jarkko Sakkinen

//! Separate file descriptor for landstrip trap response blocks.

use std::path::Path;

#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct TrapFd {
    fd: Option<i32>,
}

impl TrapFd {
    pub(crate) fn from_fd(fd: Option<i32>) -> Self {
        Self { fd }
    }

    pub(crate) fn is_enabled(self) -> bool {
        self.fd.is_some()
    }

    pub(crate) fn close(self) {
        let Some(fd) = self.fd else {
            return;
        };
        close_trap_fd(fd);
    }

    pub(crate) fn emit_filesystem_denial(self, operation: &str, path: &Path, mechanism: &str) {
        let Some(fd) = self.fd else {
            return;
        };

        let json = serde_json::json!({
            "reason": "AccessDenied",
            "type": "filesystem",
            "file": path.display().to_string(),
            "operation": operation,
            "mechanism": mechanism
        });
        let response = format!("{json}\n");
        write_trap_line(fd, response.as_bytes());
    }
}

#[cfg(unix)]
fn write_trap_line(fd: i32, line: &[u8]) {
    let mut remaining = line;
    while !remaining.is_empty() {
        // SAFETY: write(2) copies bytes from the live slice pointer.
        let written = unsafe { libc::write(fd, remaining.as_ptr().cast(), remaining.len()) };
        if written == 0 {
            return;
        }
        if written < 0 {
            let error = std::io::Error::last_os_error();
            if error.raw_os_error() == Some(libc::EINTR) {
                continue;
            }
            log::debug!(
                "trap fd write fd={fd} errno={}",
                error.raw_os_error().unwrap_or(0)
            );
            return;
        }

        let Ok(written) = usize::try_from(written) else {
            return;
        };
        remaining = &remaining[written..];
    }
}

#[cfg(unix)]
fn close_trap_fd(fd: i32) {
    // SAFETY: close(2) copies the scalar file descriptor argument.
    let rc = unsafe { libc::close(fd) };
    if rc != 0 {
        let error = std::io::Error::last_os_error();
        log::debug!(
            "trap fd close fd={fd} errno={}",
            error.raw_os_error().unwrap_or(0)
        );
    }
}

#[cfg(not(unix))]
fn write_trap_line(_fd: i32, _line: &[u8]) {}

#[cfg(not(unix))]
fn close_trap_fd(_fd: i32) {}