oci_spec/runtime/
process.rs

1use crate::{
2    error::OciSpecError,
3    runtime::{Capabilities, Capability},
4};
5use derive_builder::Builder;
6use getset::{CopyGetters, Getters, MutGetters, Setters};
7use regex::Regex;
8use serde::{de, Deserialize, Deserializer, Serialize};
9use std::path::PathBuf;
10use std::sync::OnceLock;
11use strum_macros::{Display as StrumDisplay, EnumString};
12
13#[derive(
14    Builder,
15    Clone,
16    CopyGetters,
17    Debug,
18    Deserialize,
19    Getters,
20    MutGetters,
21    Setters,
22    Eq,
23    PartialEq,
24    Serialize,
25)]
26#[serde(rename_all = "camelCase")]
27#[builder(
28    default,
29    pattern = "owned",
30    setter(into, strip_option),
31    build_fn(error = "OciSpecError")
32)]
33/// Process contains information to start a specific application inside the
34/// container.
35pub struct Process {
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    #[getset(get_copy = "pub", set = "pub")]
38    /// Terminal creates an interactive terminal for the container.
39    terminal: Option<bool>,
40
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    #[getset(get_copy = "pub", set = "pub")]
43    /// ConsoleSize specifies the size of the console.
44    console_size: Option<Box>,
45
46    #[getset(get_mut = "pub", get = "pub", set = "pub")]
47    /// User specifies user information for the process.
48    user: User,
49
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    #[getset(get = "pub", set = "pub")]
52    /// Args specifies the binary and arguments for the application to
53    /// execute.
54    args: Option<Vec<String>>,
55
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    #[getset(get_mut = "pub", get = "pub", set = "pub")]
58    /// CommandLine specifies the full command line for the application to
59    /// execute on Windows.
60    command_line: Option<String>,
61
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    #[getset(get_mut = "pub", get = "pub", set = "pub")]
64    /// Env populates the process environment for the process.
65    env: Option<Vec<String>>,
66
67    #[getset(get = "pub", set = "pub")]
68    /// Cwd is the current working directory for the process and must be
69    /// relative to the container's root.
70    cwd: PathBuf,
71
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    #[getset(get = "pub", set = "pub")]
74    /// Capabilities are Linux capabilities that are kept for the process.
75    capabilities: Option<LinuxCapabilities>,
76
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    #[getset(get = "pub", set = "pub")]
79    /// Rlimits specifies rlimit options to apply to the process.
80    rlimits: Option<Vec<PosixRlimit>>,
81
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    #[getset(get_copy = "pub", set = "pub")]
84    /// NoNewPrivileges controls whether additional privileges could be
85    /// gained by processes in the container.
86    no_new_privileges: Option<bool>,
87
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    #[getset(get = "pub", set = "pub")]
90    /// ApparmorProfile specifies the apparmor profile for the container.
91    apparmor_profile: Option<String>,
92
93    #[serde(skip_serializing_if = "Option::is_none")]
94    #[getset(get_copy = "pub", set = "pub")]
95    /// Specify an oom_score_adj for the container.
96    oom_score_adj: Option<i32>,
97
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    #[getset(get = "pub", set = "pub")]
100    /// SelinuxLabel specifies the selinux context that the container
101    /// process is run as.
102    selinux_label: Option<String>,
103
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    #[getset(get = "pub", set = "pub")]
106    /// IOPriority contains the I/O priority settings for the cgroup.
107    io_priority: Option<LinuxIOPriority>,
108
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    #[getset(get = "pub", set = "pub")]
111    /// Scheduler specifies the scheduling attributes for a process
112    scheduler: Option<Scheduler>,
113
114    #[serde(
115        rename = "execCPUAffinity",
116        default,
117        skip_serializing_if = "Option::is_none"
118    )]
119    #[getset(get = "pub", set = "pub")]
120    /// ExecCPUAffinity specifies the cpu affinity for a process
121    exec_cpu_affinity: Option<ExecCPUAffinity>,
122}
123
124// Default impl for processes in the container
125impl Default for Process {
126    fn default() -> Self {
127        Process {
128            // Don't create an interactive terminal for container by default
129            terminal: false.into(),
130            // Gives default console size of 0, 0
131            console_size: Default::default(),
132            // Gives process a uid and gid of 0 (root)
133            user: Default::default(),
134            // By default executes sh command, giving user shell
135            args: vec!["sh".to_string()].into(),
136            // Sets linux default environment for binaries and default xterm emulator
137            env: vec![
138                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into(),
139                "TERM=xterm".into(),
140            ]
141            .into(),
142            // Sets cwd of process to the container root by default
143            cwd: "/".into(),
144            // By default does not allow process to gain additional privileges
145            no_new_privileges: true.into(),
146            // Empty String, no default apparmor
147            apparmor_profile: Default::default(),
148            // Empty String, no default selinux
149            selinux_label: Default::default(),
150            // Empty String, no default scheduler
151            scheduler: Default::default(),
152            // See impl Default for LinuxCapabilities
153            capabilities: Some(Default::default()),
154            // Sets the default maximum of 1024 files the process can open
155            // This is the same as the linux kernel default
156            rlimits: vec![PosixRlimit {
157                typ: PosixRlimitType::RlimitNofile,
158                hard: 1024,
159                soft: 1024,
160            }]
161            .into(),
162            oom_score_adj: None,
163            command_line: None,
164            // Empty IOPriority, no default iopriority
165            io_priority: Default::default(),
166            exec_cpu_affinity: Default::default(),
167        }
168    }
169}
170
171#[derive(
172    Builder, Clone, Copy, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
173)]
174#[builder(
175    default,
176    pattern = "owned",
177    setter(into, strip_option),
178    build_fn(error = "OciSpecError")
179)]
180#[getset(get_copy = "pub", set = "pub")]
181/// Box specifies dimensions of a rectangle. Used for specifying the size of
182/// a console.
183pub struct Box {
184    #[serde(default)]
185    /// Height is the vertical dimension of a box.
186    height: u64,
187
188    #[serde(default)]
189    /// Width is the horizontal dimension of a box.
190    width: u64,
191}
192
193#[derive(
194    Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString,
195)]
196#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
197#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
198/// Available rlimit types (see <https://man7.org/linux/man-pages/man2/getrlimit.2.html>)
199pub enum PosixRlimitType {
200    /// Limit in seconds of the amount of CPU time that the process can consume.
201    #[default]
202    RlimitCpu,
203
204    /// Maximum size in bytes of the files that the process creates.
205    RlimitFsize,
206
207    /// Maximum size of the process's data segment (init data, uninit data and
208    /// heap) in bytes.
209    RlimitData,
210
211    /// Maximum size of the process stack in bytes.
212    RlimitStack,
213
214    /// Maximum size of a core dump file in bytes.
215    RlimitCore,
216
217    /// Limit on the process's resident set (the number of virtual pages
218    /// resident in RAM).
219    RlimitRss,
220
221    /// Limit on number of threads for the real uid calling processes.
222    RlimitNproc,
223
224    /// One greator than the maximum number of file descriptors that one process
225    /// may open.
226    RlimitNofile,
227
228    /// Maximum number of bytes of memory that may be locked into RAM.
229    RlimitMemlock,
230
231    /// Maximum size of the process's virtual memory(address space) in bytes.
232    RlimitAs,
233
234    /// Limit on the number of locks and leases for the process.
235    RlimitLocks,
236
237    /// Limit on number of signals that may be queued for the process.
238    RlimitSigpending,
239
240    /// Limit on the number of bytes that can be allocated for POSIX message
241    /// queue.
242    RlimitMsgqueue,
243
244    /// Specifies a ceiling to which the process's nice value can be raised.
245    RlimitNice,
246
247    /// Specifies a ceiling on the real-time priority.
248    RlimitRtprio,
249
250    /// This is a limit (in microseconds) on the amount of CPU time that a
251    /// process scheduled under a real-time scheduling policy may consume
252    /// without making a blocking system call.
253    RlimitRttime,
254}
255
256#[derive(
257    Builder, Clone, Copy, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
258)]
259#[builder(
260    default,
261    pattern = "owned",
262    setter(into, strip_option),
263    build_fn(error = "OciSpecError")
264)]
265#[getset(get_copy = "pub", set = "pub")]
266/// RLimit types and restrictions.
267pub struct PosixRlimit {
268    #[serde(rename = "type")]
269    /// Type of Rlimit to set
270    typ: PosixRlimitType,
271
272    #[serde(default)]
273    /// Hard limit for specified type
274    hard: u64,
275
276    #[serde(default)]
277    /// Soft limit for specified type
278    soft: u64,
279}
280
281#[derive(
282    Builder,
283    Clone,
284    CopyGetters,
285    Debug,
286    Default,
287    Deserialize,
288    Getters,
289    MutGetters,
290    Setters,
291    Eq,
292    PartialEq,
293    Serialize,
294)]
295#[serde(rename_all = "camelCase")]
296#[builder(
297    default,
298    pattern = "owned",
299    setter(into, strip_option),
300    build_fn(error = "OciSpecError")
301)]
302/// User id (uid) and group id (gid) tracks file permissions.
303pub struct User {
304    #[serde(default)]
305    #[getset(get_mut = "pub", get_copy = "pub", set = "pub")]
306    /// UID is the user id.
307    uid: u32,
308
309    #[serde(default)]
310    #[getset(get_mut = "pub", get_copy = "pub", set = "pub")]
311    /// GID is the group id.
312    gid: u32,
313
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    #[getset(get_mut = "pub", get_copy = "pub", set = "pub")]
316    /// Specifies the umask of the user.
317    umask: Option<u32>,
318
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    #[getset(get_mut = "pub", get = "pub", set = "pub")]
321    /// AdditionalGids are additional group ids set for the container's
322    /// process.
323    additional_gids: Option<Vec<u32>>,
324
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    #[getset(get_mut = "pub", get = "pub", set = "pub")]
327    /// Username is the user name.
328    username: Option<String>,
329}
330
331#[derive(Builder, Clone, Debug, Deserialize, Getters, Setters, Eq, PartialEq, Serialize)]
332#[builder(
333    default,
334    pattern = "owned",
335    setter(into, strip_option),
336    build_fn(error = "OciSpecError")
337)]
338#[getset(get = "pub", set = "pub")]
339/// LinuxCapabilities specifies the list of allowed capabilities that are
340/// kept for a process. <http://man7.org/linux/man-pages/man7/capabilities.7.html>
341pub struct LinuxCapabilities {
342    #[serde(default, skip_serializing_if = "Option::is_none")]
343    /// Bounding is the set of capabilities checked by the kernel.
344    bounding: Option<Capabilities>,
345
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    /// Effective is the set of capabilities checked by the kernel.
348    effective: Option<Capabilities>,
349
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    /// Inheritable is the capabilities preserved across execve.
352    inheritable: Option<Capabilities>,
353
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    /// Permitted is the limiting superset for effective capabilities.
356    permitted: Option<Capabilities>,
357
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    /// Ambient is the ambient set of capabilities that are kept.
360    ambient: Option<Capabilities>,
361}
362
363// Default container's linux capabilities:
364// CAP_AUDIT_WRITE gives container ability to write to linux audit logs,
365// CAP_KILL gives container ability to kill non root processes
366// CAP_NET_BIND_SERVICE allows container to bind to ports below 1024
367impl Default for LinuxCapabilities {
368    fn default() -> Self {
369        let audit_write = Capability::AuditWrite;
370        let cap_kill = Capability::Kill;
371        let net_bind = Capability::NetBindService;
372        let default_vec = vec![audit_write, cap_kill, net_bind]
373            .into_iter()
374            .collect::<Capabilities>();
375        LinuxCapabilities {
376            bounding: default_vec.clone().into(),
377            effective: default_vec.clone().into(),
378            inheritable: default_vec.clone().into(),
379            permitted: default_vec.clone().into(),
380            ambient: default_vec.into(),
381        }
382    }
383}
384
385#[derive(
386    Builder, Clone, Copy, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
387)]
388#[builder(
389    default,
390    pattern = "owned",
391    setter(into, strip_option),
392    build_fn(error = "OciSpecError")
393)]
394#[getset(get_copy = "pub", set = "pub")]
395/// RLimit types and restrictions.
396pub struct LinuxIOPriority {
397    #[serde(default)]
398    /// Class represents an I/O scheduling class.
399    class: IOPriorityClass,
400
401    #[serde(default)]
402    /// Priority for the io operation
403    priority: i64,
404}
405
406#[derive(
407    Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString,
408)]
409#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
410#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
411/// IOPriorityClass represents an I/O scheduling class.
412pub enum IOPriorityClass {
413    /// This is the realtime io class. This scheduling class is given
414    /// higher priority than any other in the system, processes from this class are
415    /// given first access to the disk every time. Thus it needs to be used with some
416    /// care, one io RT process can starve the entire system. Within the RT class,
417    /// there are 8 levels of class data that determine exactly how much time this
418    /// process needs the disk for on each service. In the future this might change
419    /// to be more directly mappable to performance, by passing in a wanted data
420    /// rate instead
421    IoprioClassRt,
422    /// This is the best-effort scheduling class, which is the default
423    /// for any process that hasn't set a specific io priority. The class data
424    /// determines how much io bandwidth the process will get, it's directly mappable
425    /// to the cpu nice levels just more coarsely implemented. 0 is the highest
426    /// BE prio level, 7 is the lowest. The mapping between cpu nice level and io
427    /// nice level is determined as: io_nice = (cpu_nice + 20) / 5.
428    #[default]
429    IoprioClassBe,
430    /// This is the idle scheduling class, processes running at this
431    /// level only get io time when no one else needs the disk. The idle class has no
432    /// class data, since it doesn't really apply here.
433    IoprioClassIdle,
434}
435
436#[derive(Builder, Clone, Debug, Deserialize, Getters, Setters, Eq, PartialEq, Serialize)]
437#[builder(
438    default,
439    pattern = "owned",
440    setter(into, strip_option),
441    build_fn(error = "OciSpecError")
442)]
443#[getset(get = "pub", set = "pub")]
444/// Scheduler represents the scheduling attributes for a process. It is based on
445/// the Linux sched_setattr(2) syscall.
446pub struct Scheduler {
447    /// Policy represents the scheduling policy (e.g., SCHED_FIFO, SCHED_RR, SCHED_OTHER).
448    policy: LinuxSchedulerPolicy,
449
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    /// Nice is the nice value for the process, which affects its priority.
452    nice: Option<i32>,
453
454    #[serde(default, skip_serializing_if = "Option::is_none")]
455    /// Priority represents the static priority of the process.
456    priority: Option<i32>,
457
458    #[serde(default, skip_serializing_if = "Option::is_none")]
459    /// Flags is an array of scheduling flags.
460    flags: Option<Vec<LinuxSchedulerFlag>>,
461
462    // The following ones are used by the DEADLINE scheduler.
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    /// Runtime is the amount of time in nanoseconds during which the process
465    /// is allowed to run in a given period.
466    runtime: Option<u64>,
467
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    /// Deadline is the absolute deadline for the process to complete its execution.
470    deadline: Option<u64>,
471
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    /// Period is the length of the period in nanoseconds used for determining the process runtime.
474    period: Option<u64>,
475}
476
477/// Default scheduler is SCHED_OTHER with no priority.
478impl Default for Scheduler {
479    fn default() -> Self {
480        Self {
481            policy: LinuxSchedulerPolicy::default(),
482            nice: None,
483            priority: None,
484            flags: None,
485            runtime: None,
486            deadline: None,
487            period: None,
488        }
489    }
490}
491
492#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString)]
493#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
494#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
495///  LinuxSchedulerPolicy represents different scheduling policies used with the Linux Scheduler
496pub enum LinuxSchedulerPolicy {
497    /// SchedOther is the default scheduling policy
498    SchedOther,
499    /// SchedFIFO is the First-In-First-Out scheduling policy
500    SchedFifo,
501    /// SchedRR is the Round-Robin scheduling policy
502    SchedRr,
503    /// SchedBatch is the Batch scheduling policy
504    SchedBatch,
505    /// SchedISO is the Isolation scheduling policy
506    SchedIso,
507    /// SchedIdle is the Idle scheduling policy
508    SchedIdle,
509    /// SchedDeadline is the Deadline scheduling policy
510    SchedDeadline,
511}
512
513/// Default LinuxSchedulerPolicy is SchedOther
514impl Default for LinuxSchedulerPolicy {
515    fn default() -> Self {
516        LinuxSchedulerPolicy::SchedOther
517    }
518}
519
520#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString)]
521#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
522#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
523///  LinuxSchedulerFlag represents the flags used by the Linux Scheduler.
524pub enum LinuxSchedulerFlag {
525    /// SchedFlagResetOnFork represents the reset on fork scheduling flag
526    SchedResetOnFork,
527    /// SchedFlagReclaim represents the reclaim scheduling flag
528    SchedFlagReclaim,
529    /// SchedFlagDLOverrun represents the deadline overrun scheduling flag
530    SchedFlagDLOverrun,
531    /// SchedFlagKeepPolicy represents the keep policy scheduling flag
532    SchedFlagKeepPolicy,
533    /// SchedFlagKeepParams represents the keep parameters scheduling flag
534    SchedFlagKeepParams,
535    /// SchedFlagUtilClampMin represents the utilization clamp minimum scheduling flag
536    SchedFlagUtilClampMin,
537    /// SchedFlagUtilClampMin represents the utilization clamp maximum scheduling flag
538    SchedFlagUtilClampMax,
539}
540
541/// Default LinuxSchedulerFlag is SchedResetOnFork
542impl Default for LinuxSchedulerFlag {
543    fn default() -> Self {
544        LinuxSchedulerFlag::SchedResetOnFork
545    }
546}
547
548#[derive(
549    Builder, Clone, Debug, Default, Deserialize, Getters, Setters, Eq, PartialEq, Serialize,
550)]
551#[builder(
552    default,
553    pattern = "owned",
554    setter(into, strip_option),
555    build_fn(validate = "Self::validate", error = "OciSpecError")
556)]
557#[getset(get = "pub", set = "pub")]
558/// ExecCPUAffinity specifies CPU affinity used to execute the process.
559/// This setting is not applicable to the container's init process.
560pub struct ExecCPUAffinity {
561    #[serde(
562        default,
563        skip_serializing_if = "Option::is_none",
564        deserialize_with = "deserialize"
565    )]
566    /// initial is a list of CPUs a runtime parent process to be run on
567    /// initially, before the transition to container's cgroup.
568    /// This is a a comma-separated list, with dashes to represent ranges.
569    /// For example, `0-3,7` represents CPUs 0,1,2,3, and 7.
570    initial: Option<String>,
571
572    #[serde(
573        default,
574        rename = "final",
575        skip_serializing_if = "Option::is_none",
576        deserialize_with = "deserialize"
577    )]
578    /// cpu_affinity_final is a list of CPUs the process will be run on after the transition
579    /// to container's cgroup. The format is the same as for `initial`. If omitted or empty,
580    /// runtime SHOULD NOT change process' CPU affinity after the process is moved to
581    /// container's cgroup, and the final affinity is determined by the Linux kernel.
582    cpu_affinity_final: Option<String>,
583}
584
585impl ExecCPUAffinityBuilder {
586    fn validate(&self) -> Result<(), OciSpecError> {
587        if let Some(Some(ref s)) = self.initial {
588            validate_cpu_affinity(s).map_err(|e| OciSpecError::Other(e.to_string()))?;
589        }
590
591        if let Some(Some(ref s)) = self.cpu_affinity_final {
592            validate_cpu_affinity(s).map_err(|e| OciSpecError::Other(e.to_string()))?;
593        }
594
595        Ok(())
596    }
597}
598
599fn deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
600where
601    D: Deserializer<'de>,
602{
603    let value: Option<String> = Option::deserialize(deserializer)?;
604
605    if let Some(ref s) = value {
606        validate_cpu_affinity(s).map_err(de::Error::custom)?;
607    }
608
609    Ok(value)
610}
611
612fn exec_cpu_affinity_regex() -> &'static Regex {
613    static EXEC_CPU_AFFINITY_REGEX: OnceLock<Regex> = OnceLock::new();
614    EXEC_CPU_AFFINITY_REGEX.get_or_init(|| {
615        Regex::new(r"^(\d+(-\d+)?)(,\d+(-\d+)?)*$")
616            .expect("Failed to create regex for execCPUAffinity")
617    })
618}
619
620fn validate_cpu_affinity(s: &str) -> Result<(), String> {
621    if !exec_cpu_affinity_regex().is_match(s) {
622        return Err(format!("Invalid execCPUAffinity format: {s}"));
623    }
624
625    Ok(())
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631    use serde_json::json;
632
633    // PosixRlimitType test cases
634    #[test]
635    fn posix_rlimit_type_enum_to_string() {
636        let type_a = PosixRlimitType::RlimitCpu;
637        assert_eq!(type_a.to_string(), "RLIMIT_CPU");
638
639        let type_b = PosixRlimitType::RlimitData;
640        assert_eq!(type_b.to_string(), "RLIMIT_DATA");
641
642        let type_c = PosixRlimitType::RlimitNofile;
643        assert_eq!(type_c.to_string(), "RLIMIT_NOFILE");
644    }
645
646    #[test]
647    fn posix_rlimit_type_string_to_enum() {
648        let posix_rlimit_type_str = "RLIMIT_CPU";
649        let posix_rlimit_type_enum: PosixRlimitType = posix_rlimit_type_str.parse().unwrap();
650        assert_eq!(posix_rlimit_type_enum, PosixRlimitType::RlimitCpu);
651
652        let posix_rlimit_type_str = "RLIMIT_DATA";
653        let posix_rlimit_type_enum: PosixRlimitType = posix_rlimit_type_str.parse().unwrap();
654        assert_eq!(posix_rlimit_type_enum, PosixRlimitType::RlimitData);
655
656        let posix_rlimit_type_str = "RLIMIT_NOFILE";
657        let posix_rlimit_type_enum: PosixRlimitType = posix_rlimit_type_str.parse().unwrap();
658        assert_eq!(posix_rlimit_type_enum, PosixRlimitType::RlimitNofile);
659
660        let invalid_posix_rlimit_type_str = "x";
661        let unknown_rlimit = invalid_posix_rlimit_type_str.parse::<PosixRlimitType>();
662        assert!(unknown_rlimit.is_err());
663    }
664
665    #[test]
666    fn exec_cpu_affinity_valid_initial_final() {
667        let json = json!({"initial": "0-3,7", "final": "4-6,8"});
668        let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
669        assert!(result.is_ok());
670
671        let json = json!({"initial": "0-3", "final": "4-6"});
672        let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
673        assert!(result.is_ok());
674
675        let json = json!({"initial": "0", "final": "4"});
676        let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
677        assert!(result.is_ok());
678    }
679
680    #[test]
681    fn exec_cpu_affinity_invalid_initial() {
682        let json = json!({"initial": "0-3,,7", "final": "4-6,8"});
683        let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
684        assert!(result.is_err());
685    }
686
687    #[test]
688    fn exec_cpu_affinity_invalid_final() {
689        let json = json!({"initial": "0-3,7", "final": "4-6.,8"});
690        let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
691        assert!(result.is_err());
692    }
693
694    #[test]
695    fn exec_cpu_affinity_valid_final() {
696        let json = json!({"final": "0,1,2,3"});
697        let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
698        assert!(result.is_ok());
699        assert!(result.unwrap().initial.is_none());
700    }
701
702    #[test]
703    fn exec_cpu_affinity_valid_initial() {
704        let json = json!({"initial": "0-1,2-5"});
705        let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
706        assert!(result.is_ok());
707        assert!(result.unwrap().cpu_affinity_final.is_none());
708    }
709
710    #[test]
711    fn exec_cpu_affinity_empty() {
712        let json = json!({});
713        let result: Result<ExecCPUAffinity, _> = serde_json::from_value(json);
714        assert!(result.is_ok());
715        let affinity = result.unwrap();
716        assert!(affinity.initial.is_none());
717        assert!(affinity.cpu_affinity_final.is_none());
718    }
719
720    #[test]
721    fn test_build_valid_input() {
722        let affinity = ExecCPUAffinityBuilder::default()
723            .initial("0-3,7,8,9,10".to_string())
724            .cpu_affinity_final("4-6,8".to_string())
725            .build();
726        assert!(affinity.is_ok());
727        let affinity = affinity.unwrap();
728        assert_eq!(affinity.initial, Some("0-3,7,8,9,10".to_string()));
729        assert_eq!(affinity.cpu_affinity_final, Some("4-6,8".to_string()));
730    }
731
732    #[test]
733    fn test_build_invalid_initial() {
734        let affinity = ExecCPUAffinityBuilder::default()
735            .initial("0-3,i".to_string())
736            .cpu_affinity_final("4-6,8".to_string())
737            .build();
738        let err = affinity.unwrap_err();
739        assert_eq!(err.to_string(), "Invalid execCPUAffinity format: 0-3,i");
740
741        let affinity = ExecCPUAffinityBuilder::default()
742            .initial("-".to_string())
743            .cpu_affinity_final("4-6,8".to_string())
744            .build();
745        let err = affinity.unwrap_err();
746        assert_eq!(err.to_string(), "Invalid execCPUAffinity format: -");
747    }
748
749    #[test]
750    fn test_build_invalid_final() {
751        let affinity = ExecCPUAffinityBuilder::default()
752            .initial("0-3,7".to_string())
753            .cpu_affinity_final("0-l1".to_string())
754            .build();
755        let err = affinity.unwrap_err();
756        assert_eq!(err.to_string(), "Invalid execCPUAffinity format: 0-l1");
757
758        let affinity = ExecCPUAffinityBuilder::default()
759            .initial("0-3,7".to_string())
760            .cpu_affinity_final(",1,2".to_string())
761            .build();
762        let err = affinity.unwrap_err();
763        assert_eq!(err.to_string(), "Invalid execCPUAffinity format: ,1,2");
764    }
765
766    #[test]
767    fn test_build_empty() {
768        let affinity = ExecCPUAffinityBuilder::default().build();
769        let affinity = affinity.unwrap();
770        assert!(affinity.initial.is_none());
771        assert!(affinity.cpu_affinity_final.is_none());
772    }
773}