use hashbrown::HashMap;
use std::ffi::OsString;
use std::path::PathBuf;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use super::SandboxPermissions;
#[derive(Debug, Clone, Default)]
pub enum ExecExpiration {
Timeout(Duration),
#[default]
DefaultTimeout,
Cancellation(CancellationToken),
}
impl From<Option<u64>> for ExecExpiration {
fn from(timeout_ms: Option<u64>) -> Self {
match timeout_ms {
Some(ms) => Self::Timeout(Duration::from_millis(ms)),
None => Self::DefaultTimeout,
}
}
}
impl From<u64> for ExecExpiration {
fn from(timeout_ms: u64) -> Self {
Self::Timeout(Duration::from_millis(timeout_ms))
}
}
impl ExecExpiration {
pub fn timeout_ms(&self) -> Option<u64> {
match self {
Self::Timeout(d) => Some(d.as_millis() as u64),
Self::DefaultTimeout => Some(30_000), Self::Cancellation(_) => None,
}
}
pub fn timeout_duration(&self) -> Option<Duration> {
match self {
Self::Timeout(d) => Some(*d),
Self::DefaultTimeout => Some(Duration::from_secs(30)),
Self::Cancellation(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub struct CommandSpec {
pub program: OsString,
pub args: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub expiration: ExecExpiration,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
}
impl Default for CommandSpec {
fn default() -> Self {
Self {
program: OsString::new(),
args: Vec::new(),
cwd: PathBuf::new(),
env: HashMap::new(),
expiration: ExecExpiration::DefaultTimeout,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
}
}
}
impl CommandSpec {
pub fn new(program: impl Into<OsString>) -> Self {
Self {
program: program.into(),
..Default::default()
}
}
pub fn with_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.args = args.into_iter().map(Into::into).collect();
self
}
pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.cwd = cwd.into();
self
}
pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
self.env = env;
self
}
pub fn with_expiration(mut self, expiration: ExecExpiration) -> Self {
self.expiration = expiration;
self
}
pub fn with_sandbox_permissions(mut self, permissions: SandboxPermissions) -> Self {
self.sandbox_permissions = permissions;
self
}
pub fn with_justification(mut self, justification: impl Into<String>) -> Self {
self.justification = Some(justification.into());
self
}
pub fn full_command(&self) -> Vec<OsString> {
let mut cmd = vec![self.program.clone()];
cmd.extend(self.args.iter().cloned().map(OsString::from));
cmd
}
}
#[derive(Debug, Clone)]
pub struct ExecEnv {
pub program: PathBuf,
pub args: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub expiration: ExecExpiration,
pub sandbox_active: bool,
pub sandbox_type: SandboxType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SandboxType {
#[default]
None,
MacosSeatbelt,
LinuxLandlock,
WindowsRestrictedToken,
}
impl SandboxType {
pub fn platform_default() -> Self {
#[cfg(target_os = "macos")]
{
Self::MacosSeatbelt
}
#[cfg(target_os = "linux")]
{
Self::LinuxLandlock
}
#[cfg(target_os = "windows")]
{
Self::WindowsRestrictedToken
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Self::None
}
}
pub fn is_available(&self) -> bool {
match self {
Self::None => true,
Self::MacosSeatbelt => cfg!(target_os = "macos"),
Self::LinuxLandlock => cfg!(target_os = "linux"),
Self::WindowsRestrictedToken => cfg!(target_os = "windows"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_spec_builder() {
let spec = CommandSpec::new("cat")
.with_args(vec!["file.txt"])
.with_cwd("/tmp")
.with_justification("testing");
assert_eq!(spec.program, OsString::from("cat"));
assert_eq!(spec.args, vec!["file.txt"]);
assert_eq!(spec.cwd, PathBuf::from("/tmp"));
assert_eq!(spec.justification, Some("testing".to_string()));
}
#[test]
fn test_full_command() {
let spec = CommandSpec::new("echo").with_args(vec!["hello", "world"]);
assert_eq!(
spec.full_command(),
vec![
OsString::from("echo"),
OsString::from("hello"),
OsString::from("world")
]
);
}
#[test]
fn test_command_spec_accepts_path_backed_program() {
let program = PathBuf::from("/tmp/example-program");
let spec = CommandSpec::new(program.clone());
assert_eq!(spec.program, program.into_os_string());
}
#[test]
fn test_exec_expiration() {
let timeout = ExecExpiration::Timeout(Duration::from_secs(10));
assert_eq!(timeout.timeout_ms(), Some(10_000));
let default = ExecExpiration::DefaultTimeout;
assert_eq!(default.timeout_ms(), Some(30_000));
}
}