use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecRequest {
pub cmd: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: Vec<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub user: Option<String>,
#[serde(default)]
pub tty: bool,
#[serde(default = "default_rows")]
pub rows: u16,
#[serde(default = "default_cols")]
pub cols: u16,
#[serde(default)]
pub rlimits: Vec<ExecRlimit>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecRlimit {
pub resource: String,
pub soft: u64,
pub hard: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecStarted {
pub pid: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecStdin {
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecStdinError {
#[serde(default)]
pub errno: Option<i32>,
#[serde(default)]
pub errno_name: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecStdout {
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecStderr {
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecExited {
pub code: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecFailed {
pub kind: ExecFailureKind,
#[serde(default)]
pub errno: Option<i32>,
#[serde(default)]
pub errno_name: Option<String>,
pub message: String,
#[serde(default)]
pub stage: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecFailureKind {
NotFound,
PermissionDenied,
NotExecutable,
BadCwd,
BadArgs,
ResourceLimit,
UserSetupFailed,
OutOfMemory,
PtySetupFailed,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecResize {
pub rows: u16,
pub cols: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecSignal {
pub signal: i32,
}
fn default_rows() -> u16 {
24
}
fn default_cols() -> u16 {
80
}
impl FromStr for ExecRlimit {
type Err = String;
fn from_str(spec: &str) -> Result<Self, Self::Err> {
let (resource, limit) = spec
.split_once('=')
.ok_or_else(|| "rlimit must be in format RESOURCE=LIMIT".to_string())?;
let mut parts = limit.split(':');
let soft = parts
.next()
.ok_or_else(|| "missing soft limit".to_string())?
.parse::<u64>()
.map_err(|err| format!("invalid soft limit: {err}"))?;
let hard = match parts.next() {
Some(value) => value
.parse::<u64>()
.map_err(|err| format!("invalid hard limit: {err}"))?,
None => soft,
};
if parts.next().is_some() {
return Err("too many ':' separators".into());
}
if soft > hard {
return Err("soft limit cannot exceed hard limit".into());
}
Ok(Self {
resource: resource.to_ascii_lowercase(),
soft,
hard,
})
}
}
#[cfg(test)]
mod tests {
use super::ExecRlimit;
#[test]
fn test_exec_rlimit_from_str_uses_soft_for_hard_when_omitted() {
assert_eq!(
"NOFILE=65535".parse::<ExecRlimit>().unwrap(),
ExecRlimit {
resource: "nofile".to_string(),
soft: 65_535,
hard: 65_535,
}
);
}
#[test]
fn test_exec_rlimit_from_str_parses_soft_and_hard() {
assert_eq!(
"nofile=4096:65535".parse::<ExecRlimit>().unwrap(),
ExecRlimit {
resource: "nofile".to_string(),
soft: 4_096,
hard: 65_535,
}
);
}
#[test]
fn test_exec_rlimit_from_str_rejects_soft_above_hard() {
let err = "nofile=65535:4096".parse::<ExecRlimit>().unwrap_err();
assert_eq!(err, "soft limit cannot exceed hard limit");
}
}
pub use microsandbox_types::RlimitResource;