1use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ExecRequest {
14 pub cmd: String,
16
17 #[serde(default)]
19 pub args: Vec<String>,
20
21 #[serde(default)]
23 pub env: Vec<String>,
24
25 #[serde(default)]
27 pub cwd: Option<String>,
28
29 #[serde(default)]
31 pub user: Option<String>,
32
33 #[serde(default)]
35 pub tty: bool,
36
37 #[serde(default = "default_rows")]
39 pub rows: u16,
40
41 #[serde(default = "default_cols")]
43 pub cols: u16,
44
45 #[serde(default)]
47 pub rlimits: Vec<ExecRlimit>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55pub enum RlimitResource {
56 Cpu,
58 Fsize,
60 Data,
62 Stack,
64 Core,
66 Rss,
68 Nproc,
70 Nofile,
72 Memlock,
74 As,
76 Locks,
78 Sigpending,
80 Msgqueue,
82 Nice,
84 Rtprio,
86 Rttime,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct ExecRlimit {
93 pub resource: String,
95
96 pub soft: u64,
98
99 pub hard: u64,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ExecStarted {
106 pub pid: u32,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ExecStdin {
113 #[serde(with = "serde_bytes")]
115 pub data: Vec<u8>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ExecStdout {
121 #[serde(with = "serde_bytes")]
123 pub data: Vec<u8>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ExecStderr {
129 #[serde(with = "serde_bytes")]
131 pub data: Vec<u8>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ExecExited {
137 pub code: i32,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct ExecResize {
144 pub rows: u16,
146
147 pub cols: u16,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ExecSignal {
154 pub signal: i32,
156}
157
158fn default_rows() -> u16 {
163 24
164}
165
166fn default_cols() -> u16 {
167 80
168}
169
170impl RlimitResource {
175 pub fn as_str(&self) -> &'static str {
177 match self {
178 Self::Cpu => "cpu",
179 Self::Fsize => "fsize",
180 Self::Data => "data",
181 Self::Stack => "stack",
182 Self::Core => "core",
183 Self::Rss => "rss",
184 Self::Nproc => "nproc",
185 Self::Nofile => "nofile",
186 Self::Memlock => "memlock",
187 Self::As => "as",
188 Self::Locks => "locks",
189 Self::Sigpending => "sigpending",
190 Self::Msgqueue => "msgqueue",
191 Self::Nice => "nice",
192 Self::Rtprio => "rtprio",
193 Self::Rttime => "rttime",
194 }
195 }
196}
197
198impl TryFrom<&str> for RlimitResource {
204 type Error = String;
205
206 fn try_from(s: &str) -> Result<Self, Self::Error> {
207 match s.to_ascii_lowercase().as_str() {
208 "cpu" => Ok(Self::Cpu),
209 "fsize" => Ok(Self::Fsize),
210 "data" => Ok(Self::Data),
211 "stack" => Ok(Self::Stack),
212 "core" => Ok(Self::Core),
213 "rss" => Ok(Self::Rss),
214 "nproc" => Ok(Self::Nproc),
215 "nofile" => Ok(Self::Nofile),
216 "memlock" => Ok(Self::Memlock),
217 "as" => Ok(Self::As),
218 "locks" => Ok(Self::Locks),
219 "sigpending" => Ok(Self::Sigpending),
220 "msgqueue" => Ok(Self::Msgqueue),
221 "nice" => Ok(Self::Nice),
222 "rtprio" => Ok(Self::Rtprio),
223 "rttime" => Ok(Self::Rttime),
224 _ => Err(format!("unknown rlimit resource: {s}")),
225 }
226 }
227}
228
229impl FromStr for ExecRlimit {
230 type Err = String;
231
232 fn from_str(spec: &str) -> Result<Self, Self::Err> {
233 let (resource, limit) = spec
234 .split_once('=')
235 .ok_or_else(|| "rlimit must be in format RESOURCE=LIMIT".to_string())?;
236
237 let mut parts = limit.split(':');
238 let soft = parts
239 .next()
240 .ok_or_else(|| "missing soft limit".to_string())?
241 .parse::<u64>()
242 .map_err(|err| format!("invalid soft limit: {err}"))?;
243 let hard = match parts.next() {
244 Some(value) => value
245 .parse::<u64>()
246 .map_err(|err| format!("invalid hard limit: {err}"))?,
247 None => soft,
248 };
249
250 if parts.next().is_some() {
251 return Err("too many ':' separators".into());
252 }
253
254 if soft > hard {
255 return Err("soft limit cannot exceed hard limit".into());
256 }
257
258 Ok(Self {
259 resource: resource.to_ascii_lowercase(),
260 soft,
261 hard,
262 })
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::ExecRlimit;
269
270 #[test]
271 fn test_exec_rlimit_from_str_uses_soft_for_hard_when_omitted() {
272 assert_eq!(
273 "NOFILE=65535".parse::<ExecRlimit>().unwrap(),
274 ExecRlimit {
275 resource: "nofile".to_string(),
276 soft: 65_535,
277 hard: 65_535,
278 }
279 );
280 }
281
282 #[test]
283 fn test_exec_rlimit_from_str_parses_soft_and_hard() {
284 assert_eq!(
285 "nofile=4096:65535".parse::<ExecRlimit>().unwrap(),
286 ExecRlimit {
287 resource: "nofile".to_string(),
288 soft: 4_096,
289 hard: 65_535,
290 }
291 );
292 }
293
294 #[test]
295 fn test_exec_rlimit_from_str_rejects_soft_above_hard() {
296 let err = "nofile=65535:4096".parse::<ExecRlimit>().unwrap_err();
297 assert_eq!(err, "soft limit cannot exceed hard limit");
298 }
299}