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, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RlimitResource {
Cpu,
Fsize,
Data,
Stack,
Core,
Rss,
Nproc,
Nofile,
Memlock,
As,
Locks,
Sigpending,
Msgqueue,
Nice,
Rtprio,
Rttime,
}
#[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 RlimitResource {
pub fn as_str(&self) -> &'static str {
match self {
Self::Cpu => "cpu",
Self::Fsize => "fsize",
Self::Data => "data",
Self::Stack => "stack",
Self::Core => "core",
Self::Rss => "rss",
Self::Nproc => "nproc",
Self::Nofile => "nofile",
Self::Memlock => "memlock",
Self::As => "as",
Self::Locks => "locks",
Self::Sigpending => "sigpending",
Self::Msgqueue => "msgqueue",
Self::Nice => "nice",
Self::Rtprio => "rtprio",
Self::Rttime => "rttime",
}
}
}
impl TryFrom<&str> for RlimitResource {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s.to_ascii_lowercase().as_str() {
"cpu" => Ok(Self::Cpu),
"fsize" => Ok(Self::Fsize),
"data" => Ok(Self::Data),
"stack" => Ok(Self::Stack),
"core" => Ok(Self::Core),
"rss" => Ok(Self::Rss),
"nproc" => Ok(Self::Nproc),
"nofile" => Ok(Self::Nofile),
"memlock" => Ok(Self::Memlock),
"as" => Ok(Self::As),
"locks" => Ok(Self::Locks),
"sigpending" => Ok(Self::Sigpending),
"msgqueue" => Ok(Self::Msgqueue),
"nice" => Ok(Self::Nice),
"rtprio" => Ok(Self::Rtprio),
"rttime" => Ok(Self::Rttime),
_ => Err(format!("unknown rlimit resource: {s}")),
}
}
}
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");
}
}