Skip to main content

arcbox_oci/
config.rs

1//! OCI runtime-spec configuration parsing.
2//!
3//! This module implements the OCI Runtime Specification config.json format.
4//! Reference: <https://github.com/opencontainers/runtime-spec/blob/main/config.md>
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10use crate::error::{OciError, Result};
11use crate::hooks::Hooks;
12
13/// OCI specification version supported by this implementation.
14pub const OCI_VERSION: &str = "1.2.0";
15
16/// OCI runtime configuration (config.json).
17///
18/// This is the main configuration structure that defines how a container
19/// should be created and run according to the OCI runtime specification.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct Spec {
23    /// OCI specification version (`SemVer` 2.0.0 format).
24    /// REQUIRED field.
25    pub oci_version: String,
26
27    /// Container's root filesystem.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub root: Option<Root>,
30
31    /// Container process to run.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub process: Option<Process>,
34
35    /// Container hostname.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub hostname: Option<String>,
38
39    /// Container domain name.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub domainname: Option<String>,
42
43    /// Additional mounts beyond the root filesystem.
44    #[serde(default, skip_serializing_if = "Vec::is_empty")]
45    pub mounts: Vec<Mount>,
46
47    /// Lifecycle hooks.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub hooks: Option<Hooks>,
50
51    /// Arbitrary metadata annotations.
52    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
53    pub annotations: HashMap<String, String>,
54
55    /// Linux-specific configuration.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub linux: Option<Linux>,
58}
59
60impl Spec {
61    /// Load OCI spec from a config.json file.
62    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
63        let content = std::fs::read_to_string(path.as_ref())?;
64        Self::from_json(&content)
65    }
66
67    /// Parse OCI spec from JSON string.
68    pub fn from_json(json: &str) -> Result<Self> {
69        let spec: Self = serde_json::from_str(json)?;
70        spec.validate()?;
71        Ok(spec)
72    }
73
74    /// Serialize to JSON string.
75    pub fn to_json(&self) -> Result<String> {
76        Ok(serde_json::to_string_pretty(self)?)
77    }
78
79    /// Serialize to JSON and write to file.
80    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
81        let json = self.to_json()?;
82        std::fs::write(path, json)?;
83        Ok(())
84    }
85
86    /// Validate the specification.
87    pub fn validate(&self) -> Result<()> {
88        // Validate OCI version format (must be valid SemVer).
89        if self.oci_version.is_empty() {
90            return Err(OciError::MissingField("ociVersion"));
91        }
92
93        // Validate version is parseable as semver.
94        if self.oci_version.split('.').count() < 2 {
95            return Err(OciError::InvalidVersion(self.oci_version.clone()));
96        }
97
98        // Validate root if present.
99        if let Some(ref root) = self.root {
100            if root.path.is_empty() {
101                return Err(OciError::InvalidConfig(
102                    "root.path cannot be empty".to_string(),
103                ));
104            }
105        }
106
107        // Validate process if present.
108        if let Some(ref process) = self.process {
109            if process.cwd.is_empty() {
110                return Err(OciError::MissingField("process.cwd"));
111            }
112            if !process.cwd.starts_with('/') {
113                return Err(OciError::InvalidConfig(
114                    "process.cwd must be an absolute path".to_string(),
115                ));
116            }
117        }
118
119        // Validate mounts.
120        for (i, mount) in self.mounts.iter().enumerate() {
121            if mount.destination.is_empty() {
122                return Err(OciError::InvalidConfig(format!(
123                    "mounts[{i}].destination cannot be empty"
124                )));
125            }
126            if !mount.destination.starts_with('/') {
127                return Err(OciError::InvalidConfig(format!(
128                    "mounts[{i}].destination must be an absolute path"
129                )));
130            }
131        }
132
133        Ok(())
134    }
135
136    /// Create a default spec for Linux containers.
137    #[must_use]
138    pub fn default_linux() -> Self {
139        Self {
140            oci_version: OCI_VERSION.to_string(),
141            root: Some(Root {
142                path: "rootfs".to_string(),
143                readonly: false,
144            }),
145            process: Some(Process::default()),
146            hostname: None,
147            domainname: None,
148            mounts: Self::default_mounts(),
149            hooks: None,
150            annotations: HashMap::new(),
151            linux: Some(Linux::default()),
152        }
153    }
154
155    /// Returns the default mounts for a Linux container.
156    fn default_mounts() -> Vec<Mount> {
157        vec![
158            Mount {
159                destination: "/proc".to_string(),
160                source: Some("proc".to_string()),
161                mount_type: Some("proc".to_string()),
162                options: Some(vec![
163                    "nosuid".to_string(),
164                    "noexec".to_string(),
165                    "nodev".to_string(),
166                ]),
167                ..Default::default()
168            },
169            Mount {
170                destination: "/dev".to_string(),
171                source: Some("tmpfs".to_string()),
172                mount_type: Some("tmpfs".to_string()),
173                options: Some(vec![
174                    "nosuid".to_string(),
175                    "strictatime".to_string(),
176                    "mode=755".to_string(),
177                    "size=65536k".to_string(),
178                ]),
179                ..Default::default()
180            },
181            Mount {
182                destination: "/dev/pts".to_string(),
183                source: Some("devpts".to_string()),
184                mount_type: Some("devpts".to_string()),
185                options: Some(vec![
186                    "nosuid".to_string(),
187                    "noexec".to_string(),
188                    "newinstance".to_string(),
189                    "ptmxmode=0666".to_string(),
190                    "mode=0620".to_string(),
191                ]),
192                ..Default::default()
193            },
194            Mount {
195                destination: "/dev/shm".to_string(),
196                source: Some("shm".to_string()),
197                mount_type: Some("tmpfs".to_string()),
198                options: Some(vec![
199                    "nosuid".to_string(),
200                    "noexec".to_string(),
201                    "nodev".to_string(),
202                    "mode=1777".to_string(),
203                    "size=65536k".to_string(),
204                ]),
205                ..Default::default()
206            },
207            Mount {
208                destination: "/sys".to_string(),
209                source: Some("sysfs".to_string()),
210                mount_type: Some("sysfs".to_string()),
211                options: Some(vec![
212                    "nosuid".to_string(),
213                    "noexec".to_string(),
214                    "nodev".to_string(),
215                    "ro".to_string(),
216                ]),
217                ..Default::default()
218            },
219        ]
220    }
221}
222
223/// Root filesystem configuration.
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
225pub struct Root {
226    /// Path to the root filesystem (relative to bundle path).
227    /// REQUIRED field.
228    pub path: String,
229
230    /// Whether the root filesystem is read-only.
231    #[serde(default)]
232    pub readonly: bool,
233}
234
235/// Container process configuration.
236#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct Process {
239    /// Whether to allocate a terminal.
240    #[serde(default)]
241    pub terminal: bool,
242
243    /// Console size (only used if terminal is true).
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub console_size: Option<ConsoleSize>,
246
247    /// Current working directory (must be absolute path).
248    /// REQUIRED field.
249    pub cwd: String,
250
251    /// Environment variables.
252    #[serde(default, skip_serializing_if = "Vec::is_empty")]
253    pub env: Vec<String>,
254
255    /// Command arguments.
256    #[serde(default, skip_serializing_if = "Vec::is_empty")]
257    pub args: Vec<String>,
258
259    /// Resource limits (rlimits).
260    #[serde(default, skip_serializing_if = "Vec::is_empty")]
261    pub rlimits: Vec<Rlimit>,
262
263    /// `AppArmor` profile.
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub apparmor_profile: Option<String>,
266
267    /// Linux capabilities.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub capabilities: Option<Capabilities>,
270
271    /// Prevent gaining new privileges.
272    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
273    pub no_new_privileges: bool,
274
275    /// OOM score adjustment.
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub oom_score_adj: Option<i32>,
278
279    /// `SELinux` label.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub selinux_label: Option<String>,
282
283    /// User specification.
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub user: Option<User>,
286
287    /// I/O priority.
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub io_priority: Option<IoPriority>,
290
291    /// Scheduler settings.
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub scheduler: Option<Scheduler>,
294
295    /// CPU affinity for exec.
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub exec_cpu_affinity: Option<ExecCpuAffinity>,
298}
299
300impl Default for Process {
301    fn default() -> Self {
302        Self {
303            terminal: false,
304            console_size: None,
305            cwd: "/".to_string(),
306            env: vec![
307                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(),
308                "TERM=xterm".to_string(),
309            ],
310            args: vec!["sh".to_string()],
311            rlimits: vec![Rlimit {
312                rlimit_type: "RLIMIT_NOFILE".to_string(),
313                soft: 1024,
314                hard: 1024,
315            }],
316            apparmor_profile: None,
317            capabilities: Some(Capabilities::default()),
318            no_new_privileges: false,
319            oom_score_adj: None,
320            selinux_label: None,
321            user: Some(User::default()),
322            io_priority: None,
323            scheduler: None,
324            exec_cpu_affinity: None,
325        }
326    }
327}
328
329/// Console size configuration.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct ConsoleSize {
332    /// Height in characters.
333    pub height: u32,
334    /// Width in characters.
335    pub width: u32,
336}
337
338/// Resource limit (rlimit) configuration.
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct Rlimit {
341    /// Limit type (e.g., `RLIMIT_NOFILE`).
342    #[serde(rename = "type")]
343    pub rlimit_type: String,
344    /// Soft limit.
345    pub soft: u64,
346    /// Hard limit.
347    pub hard: u64,
348}
349
350/// Linux capabilities configuration.
351#[derive(Debug, Clone, Default, Serialize, Deserialize)]
352pub struct Capabilities {
353    /// Effective capabilities.
354    #[serde(default, skip_serializing_if = "Vec::is_empty")]
355    pub effective: Vec<String>,
356    /// Bounding capabilities.
357    #[serde(default, skip_serializing_if = "Vec::is_empty")]
358    pub bounding: Vec<String>,
359    /// Inheritable capabilities.
360    #[serde(default, skip_serializing_if = "Vec::is_empty")]
361    pub inheritable: Vec<String>,
362    /// Permitted capabilities.
363    #[serde(default, skip_serializing_if = "Vec::is_empty")]
364    pub permitted: Vec<String>,
365    /// Ambient capabilities.
366    #[serde(default, skip_serializing_if = "Vec::is_empty")]
367    pub ambient: Vec<String>,
368}
369
370/// User identity configuration (POSIX).
371#[derive(Debug, Clone, Default, Serialize, Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub struct User {
374    /// User ID.
375    #[serde(default)]
376    pub uid: u32,
377    /// Group ID.
378    #[serde(default)]
379    pub gid: u32,
380    /// File creation mask.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub umask: Option<u32>,
383    /// Additional group IDs.
384    #[serde(default, skip_serializing_if = "Vec::is_empty")]
385    pub additional_gids: Vec<u32>,
386}
387
388/// I/O priority configuration.
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct IoPriority {
391    /// I/O scheduling class.
392    pub class: String,
393    /// Priority within class.
394    pub priority: i32,
395}
396
397/// Process scheduler configuration.
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct Scheduler {
400    /// Scheduling policy.
401    pub policy: String,
402    /// Nice value.
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub nice: Option<i32>,
405    /// Priority.
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub priority: Option<i32>,
408    /// Scheduler flags.
409    #[serde(default, skip_serializing_if = "Vec::is_empty")]
410    pub flags: Vec<String>,
411    /// Runtime (for deadline scheduler).
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub runtime: Option<u64>,
414    /// Deadline (for deadline scheduler).
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub deadline: Option<u64>,
417    /// Period (for deadline scheduler).
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub period: Option<u64>,
420}
421
422/// CPU affinity for exec.
423#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct ExecCpuAffinity {
425    /// Initial CPU affinity.
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub initial: Option<String>,
428    /// Final CPU affinity.
429    #[serde(rename = "final", skip_serializing_if = "Option::is_none")]
430    pub final_affinity: Option<String>,
431}
432
433/// Mount configuration.
434#[derive(Debug, Clone, Default, Serialize, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct Mount {
437    /// Mount destination (absolute path in container).
438    /// REQUIRED field.
439    pub destination: String,
440
441    /// Mount source (path or device).
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub source: Option<String>,
444
445    /// Filesystem type.
446    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
447    pub mount_type: Option<String>,
448
449    /// Mount options.
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub options: Option<Vec<String>>,
452
453    /// UID mappings for idmapped mounts.
454    #[serde(default, skip_serializing_if = "Vec::is_empty")]
455    pub uid_mappings: Vec<IdMapping>,
456
457    /// GID mappings for idmapped mounts.
458    #[serde(default, skip_serializing_if = "Vec::is_empty")]
459    pub gid_mappings: Vec<IdMapping>,
460}
461
462/// ID mapping for user namespace or idmapped mounts.
463#[derive(Debug, Clone, Default, Serialize, Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct IdMapping {
466    /// Starting ID in container.
467    pub container_id: u32,
468    /// Starting ID on host.
469    pub host_id: u32,
470    /// Number of IDs to map.
471    pub size: u32,
472}
473
474/// Linux-specific container configuration.
475#[derive(Debug, Clone, Default, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct Linux {
478    /// Devices to create in container.
479    #[serde(default, skip_serializing_if = "Vec::is_empty")]
480    pub devices: Vec<Device>,
481
482    /// UID mappings for user namespace.
483    #[serde(default, skip_serializing_if = "Vec::is_empty")]
484    pub uid_mappings: Vec<IdMapping>,
485
486    /// GID mappings for user namespace.
487    #[serde(default, skip_serializing_if = "Vec::is_empty")]
488    pub gid_mappings: Vec<IdMapping>,
489
490    /// Kernel parameters (sysctl).
491    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
492    pub sysctl: HashMap<String, String>,
493
494    /// Cgroups path.
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub cgroups_path: Option<String>,
497
498    /// Resource limits.
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub resources: Option<Resources>,
501
502    /// Root filesystem propagation mode.
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub rootfs_propagation: Option<String>,
505
506    /// Seccomp configuration.
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub seccomp: Option<Seccomp>,
509
510    /// Namespaces to join or create.
511    #[serde(default, skip_serializing_if = "Vec::is_empty")]
512    pub namespaces: Vec<Namespace>,
513
514    /// Paths to mask (make inaccessible).
515    #[serde(default, skip_serializing_if = "Vec::is_empty")]
516    pub masked_paths: Vec<String>,
517
518    /// Paths to make read-only.
519    #[serde(default, skip_serializing_if = "Vec::is_empty")]
520    pub readonly_paths: Vec<String>,
521
522    /// `SELinux` mount label.
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub mount_label: Option<String>,
525
526    /// Time offsets.
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub time_offsets: Option<TimeOffsets>,
529}
530
531/// Device configuration.
532#[derive(Debug, Clone, Serialize, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct Device {
535    /// Device type (c, b, p, u).
536    #[serde(rename = "type")]
537    pub device_type: String,
538    /// Device path in container.
539    pub path: String,
540    /// Major device number.
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub major: Option<i64>,
543    /// Minor device number.
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub minor: Option<i64>,
546    /// File mode.
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub file_mode: Option<u32>,
549    /// Owner UID.
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub uid: Option<u32>,
552    /// Owner GID.
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub gid: Option<u32>,
555}
556
557/// Linux namespace configuration.
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct Namespace {
560    /// Namespace type.
561    #[serde(rename = "type")]
562    pub ns_type: NamespaceType,
563    /// Path to join existing namespace.
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub path: Option<String>,
566}
567
568/// Linux namespace types.
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
570#[serde(rename_all = "lowercase")]
571pub enum NamespaceType {
572    /// PID namespace.
573    Pid,
574    /// Network namespace.
575    Network,
576    /// Mount namespace.
577    Mount,
578    /// IPC namespace.
579    Ipc,
580    /// UTS namespace.
581    Uts,
582    /// User namespace.
583    User,
584    /// Cgroup namespace.
585    Cgroup,
586    /// Time namespace.
587    Time,
588}
589
590/// Linux cgroup resource limits.
591#[derive(Debug, Clone, Default, Serialize, Deserialize)]
592#[serde(rename_all = "camelCase")]
593pub struct Resources {
594    /// Device access rules.
595    #[serde(default, skip_serializing_if = "Vec::is_empty")]
596    pub devices: Vec<DeviceCgroup>,
597
598    /// Memory limits.
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub memory: Option<MemoryResources>,
601
602    /// CPU limits.
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub cpu: Option<CpuResources>,
605
606    /// Block I/O limits.
607    #[serde(rename = "blockIO", skip_serializing_if = "Option::is_none")]
608    pub block_io: Option<BlockIoResources>,
609
610    /// PIDs limit.
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub pids: Option<PidsResources>,
613
614    /// Huge pages limits.
615    #[serde(default, skip_serializing_if = "Vec::is_empty")]
616    pub hugepage_limits: Vec<HugepageLimit>,
617
618    /// Network priorities.
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub network: Option<NetworkResources>,
621
622    /// RDMA resources.
623    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
624    pub rdma: HashMap<String, RdmaResource>,
625}
626
627/// Device cgroup rule.
628#[derive(Debug, Clone, Serialize, Deserialize)]
629#[serde(rename_all = "camelCase")]
630pub struct DeviceCgroup {
631    /// Allow or deny.
632    pub allow: bool,
633    /// Device type (a, c, b).
634    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
635    pub device_type: Option<String>,
636    /// Major number.
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub major: Option<i64>,
639    /// Minor number.
640    #[serde(skip_serializing_if = "Option::is_none")]
641    pub minor: Option<i64>,
642    /// Access rights (r, w, m).
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub access: Option<String>,
645}
646
647/// Memory resource limits.
648#[derive(Debug, Clone, Default, Serialize, Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct MemoryResources {
651    /// Memory limit in bytes.
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub limit: Option<i64>,
654    /// Memory reservation in bytes.
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub reservation: Option<i64>,
657    /// Memory + swap limit in bytes.
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub swap: Option<i64>,
660    /// Kernel memory limit in bytes.
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub kernel: Option<i64>,
663    /// Kernel TCP memory limit in bytes.
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub kernel_tcp: Option<i64>,
666    /// Swappiness (0-100).
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub swappiness: Option<u64>,
669    /// Disable OOM killer.
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub disable_oom_killer: Option<bool>,
672    /// Use hierarchy.
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub use_hierarchy: Option<bool>,
675    /// Check before update.
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub check_before_update: Option<bool>,
678}
679
680/// CPU resource limits.
681#[derive(Debug, Clone, Default, Serialize, Deserialize)]
682#[serde(rename_all = "camelCase")]
683pub struct CpuResources {
684    /// CPU shares.
685    #[serde(skip_serializing_if = "Option::is_none")]
686    pub shares: Option<u64>,
687    /// CPU quota.
688    #[serde(skip_serializing_if = "Option::is_none")]
689    pub quota: Option<i64>,
690    /// CPU burst.
691    #[serde(skip_serializing_if = "Option::is_none")]
692    pub burst: Option<u64>,
693    /// CPU period.
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub period: Option<u64>,
696    /// Realtime runtime.
697    #[serde(skip_serializing_if = "Option::is_none")]
698    pub realtime_runtime: Option<i64>,
699    /// Realtime period.
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub realtime_period: Option<u64>,
702    /// CPU set (cores).
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub cpus: Option<String>,
705    /// Memory node set.
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub mems: Option<String>,
708    /// Idle setting.
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub idle: Option<i64>,
711}
712
713/// Block I/O resource limits.
714#[derive(Debug, Clone, Default, Serialize, Deserialize)]
715#[serde(rename_all = "camelCase")]
716pub struct BlockIoResources {
717    /// Block I/O weight.
718    #[serde(skip_serializing_if = "Option::is_none")]
719    pub weight: Option<u16>,
720    /// Leaf weight.
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub leaf_weight: Option<u16>,
723    /// Per-device weight.
724    #[serde(default, skip_serializing_if = "Vec::is_empty")]
725    pub weight_device: Vec<WeightDevice>,
726    /// Throttle read bps.
727    #[serde(default, skip_serializing_if = "Vec::is_empty")]
728    pub throttle_read_bps_device: Vec<ThrottleDevice>,
729    /// Throttle write bps.
730    #[serde(default, skip_serializing_if = "Vec::is_empty")]
731    pub throttle_write_bps_device: Vec<ThrottleDevice>,
732    /// Throttle read iops.
733    #[serde(default, skip_serializing_if = "Vec::is_empty")]
734    pub throttle_read_iops_device: Vec<ThrottleDevice>,
735    /// Throttle write iops.
736    #[serde(default, skip_serializing_if = "Vec::is_empty")]
737    pub throttle_write_iops_device: Vec<ThrottleDevice>,
738}
739
740/// Block I/O weight per device.
741#[derive(Debug, Clone, Serialize, Deserialize)]
742#[serde(rename_all = "camelCase")]
743pub struct WeightDevice {
744    /// Major device number.
745    pub major: i64,
746    /// Minor device number.
747    pub minor: i64,
748    /// Weight.
749    #[serde(skip_serializing_if = "Option::is_none")]
750    pub weight: Option<u16>,
751    /// Leaf weight.
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub leaf_weight: Option<u16>,
754}
755
756/// Block I/O throttle per device.
757#[derive(Debug, Clone, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct ThrottleDevice {
760    /// Major device number.
761    pub major: i64,
762    /// Minor device number.
763    pub minor: i64,
764    /// Rate limit.
765    pub rate: u64,
766}
767
768/// PIDs resource limits.
769#[derive(Debug, Clone, Default, Serialize, Deserialize)]
770pub struct PidsResources {
771    /// Maximum number of PIDs.
772    pub limit: i64,
773}
774
775/// Hugepage limit.
776#[derive(Debug, Clone, Serialize, Deserialize)]
777#[serde(rename_all = "camelCase")]
778pub struct HugepageLimit {
779    /// Page size (e.g., "2MB", "1GB").
780    pub page_size: String,
781    /// Limit in bytes.
782    pub limit: u64,
783}
784
785/// Network resource limits.
786#[derive(Debug, Clone, Default, Serialize, Deserialize)]
787pub struct NetworkResources {
788    /// Class ID for network traffic.
789    #[serde(rename = "classID", skip_serializing_if = "Option::is_none")]
790    pub class_id: Option<u32>,
791    /// Network priorities.
792    #[serde(default, skip_serializing_if = "Vec::is_empty")]
793    pub priorities: Vec<NetworkPriority>,
794}
795
796/// Network priority.
797#[derive(Debug, Clone, Serialize, Deserialize)]
798pub struct NetworkPriority {
799    /// Interface name.
800    pub name: String,
801    /// Priority.
802    pub priority: u32,
803}
804
805/// RDMA resource limits.
806#[derive(Debug, Clone, Default, Serialize, Deserialize)]
807#[serde(rename_all = "camelCase")]
808pub struct RdmaResource {
809    /// HCA handles.
810    #[serde(skip_serializing_if = "Option::is_none")]
811    pub hca_handles: Option<u32>,
812    /// HCA objects.
813    #[serde(skip_serializing_if = "Option::is_none")]
814    pub hca_objects: Option<u32>,
815}
816
817/// Seccomp configuration.
818#[derive(Debug, Clone, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820pub struct Seccomp {
821    /// Default action.
822    pub default_action: String,
823    /// Default errno return value.
824    #[serde(skip_serializing_if = "Option::is_none")]
825    pub default_errno_ret: Option<u32>,
826    /// Architectures.
827    #[serde(default, skip_serializing_if = "Vec::is_empty")]
828    pub architectures: Vec<String>,
829    /// Flags.
830    #[serde(default, skip_serializing_if = "Vec::is_empty")]
831    pub flags: Vec<String>,
832    /// Listener path.
833    #[serde(skip_serializing_if = "Option::is_none")]
834    pub listener_path: Option<String>,
835    /// Listener metadata.
836    #[serde(skip_serializing_if = "Option::is_none")]
837    pub listener_metadata: Option<String>,
838    /// Syscall rules.
839    #[serde(default, skip_serializing_if = "Vec::is_empty")]
840    pub syscalls: Vec<SyscallRule>,
841}
842
843/// Seccomp syscall rule.
844#[derive(Debug, Clone, Serialize, Deserialize)]
845#[serde(rename_all = "camelCase")]
846pub struct SyscallRule {
847    /// Syscall names.
848    pub names: Vec<String>,
849    /// Action to take.
850    pub action: String,
851    /// Errno return value.
852    #[serde(skip_serializing_if = "Option::is_none")]
853    pub errno_ret: Option<u32>,
854    /// Arguments.
855    #[serde(default, skip_serializing_if = "Vec::is_empty")]
856    pub args: Vec<SyscallArg>,
857}
858
859/// Seccomp syscall argument filter.
860#[derive(Debug, Clone, Serialize, Deserialize)]
861#[serde(rename_all = "camelCase")]
862pub struct SyscallArg {
863    /// Argument index.
864    pub index: u32,
865    /// Value to compare.
866    pub value: u64,
867    /// Secondary value (for masked equality).
868    #[serde(skip_serializing_if = "Option::is_none")]
869    pub value_two: Option<u64>,
870    /// Comparison operator.
871    pub op: String,
872}
873
874/// Time offsets configuration.
875#[derive(Debug, Clone, Default, Serialize, Deserialize)]
876pub struct TimeOffsets {
877    /// Monotonic clock offset.
878    #[serde(skip_serializing_if = "Option::is_none")]
879    pub monotonic: Option<TimeOffset>,
880    /// Boottime clock offset.
881    #[serde(skip_serializing_if = "Option::is_none")]
882    pub boottime: Option<TimeOffset>,
883}
884
885/// Time offset value.
886#[derive(Debug, Clone, Serialize, Deserialize)]
887pub struct TimeOffset {
888    /// Seconds offset.
889    pub secs: i64,
890    /// Nanoseconds offset.
891    pub nanosecs: u32,
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897
898    #[test]
899    fn test_parse_minimal_spec() {
900        let json = r#"{
901            "ociVersion": "1.2.0"
902        }"#;
903
904        let spec = Spec::from_json(json).unwrap();
905        assert_eq!(spec.oci_version, "1.2.0");
906        assert!(spec.root.is_none());
907        assert!(spec.process.is_none());
908    }
909
910    #[test]
911    fn test_parse_spec_with_root() {
912        let json = r#"{
913            "ociVersion": "1.2.0",
914            "root": {
915                "path": "rootfs",
916                "readonly": true
917            }
918        }"#;
919
920        let spec = Spec::from_json(json).unwrap();
921        let root = spec.root.unwrap();
922        assert_eq!(root.path, "rootfs");
923        assert!(root.readonly);
924    }
925
926    #[test]
927    fn test_parse_spec_with_process() {
928        let json = r#"{
929            "ociVersion": "1.2.0",
930            "process": {
931                "terminal": true,
932                "cwd": "/app",
933                "args": ["./start.sh"],
934                "env": ["PATH=/usr/bin", "HOME=/root"]
935            }
936        }"#;
937
938        let spec = Spec::from_json(json).unwrap();
939        let process = spec.process.unwrap();
940        assert!(process.terminal);
941        assert_eq!(process.cwd, "/app");
942        assert_eq!(process.args, vec!["./start.sh"]);
943        assert_eq!(process.env.len(), 2);
944    }
945
946    #[test]
947    fn test_invalid_cwd() {
948        let json = r#"{
949            "ociVersion": "1.2.0",
950            "process": {
951                "cwd": "relative/path"
952            }
953        }"#;
954
955        let result = Spec::from_json(json);
956        assert!(result.is_err());
957    }
958
959    #[test]
960    fn test_default_linux_spec() {
961        let spec = Spec::default_linux();
962        assert_eq!(spec.oci_version, OCI_VERSION);
963        assert!(spec.root.is_some());
964        assert!(spec.process.is_some());
965        assert!(!spec.mounts.is_empty());
966        assert!(spec.linux.is_some());
967    }
968
969    #[test]
970    fn test_serialize_roundtrip() {
971        let spec = Spec::default_linux();
972        let json = spec.to_json().unwrap();
973        let parsed = Spec::from_json(&json).unwrap();
974        assert_eq!(spec.oci_version, parsed.oci_version);
975    }
976
977    #[test]
978    fn test_invalid_oci_version_empty() {
979        let json = r#"{
980            "ociVersion": ""
981        }"#;
982        let result = Spec::from_json(json);
983        assert!(result.is_err());
984    }
985
986    #[test]
987    fn test_invalid_oci_version_format() {
988        let json = r#"{
989            "ociVersion": "invalid"
990        }"#;
991        let result = Spec::from_json(json);
992        assert!(result.is_err());
993    }
994
995    #[test]
996    fn test_empty_root_path() {
997        let json = r#"{
998            "ociVersion": "1.2.0",
999            "root": {
1000                "path": ""
1001            }
1002        }"#;
1003        let result = Spec::from_json(json);
1004        assert!(result.is_err());
1005    }
1006
1007    #[test]
1008    fn test_empty_process_cwd() {
1009        let json = r#"{
1010            "ociVersion": "1.2.0",
1011            "process": {
1012                "cwd": ""
1013            }
1014        }"#;
1015        let result = Spec::from_json(json);
1016        assert!(result.is_err());
1017    }
1018
1019    #[test]
1020    fn test_invalid_mount_destination_relative() {
1021        let json = r#"{
1022            "ociVersion": "1.2.0",
1023            "mounts": [
1024                {
1025                    "destination": "relative/path",
1026                    "source": "/source"
1027                }
1028            ]
1029        }"#;
1030        let result = Spec::from_json(json);
1031        assert!(result.is_err());
1032    }
1033
1034    #[test]
1035    fn test_invalid_mount_destination_empty() {
1036        let json = r#"{
1037            "ociVersion": "1.2.0",
1038            "mounts": [
1039                {
1040                    "destination": "",
1041                    "source": "/source"
1042                }
1043            ]
1044        }"#;
1045        let result = Spec::from_json(json);
1046        assert!(result.is_err());
1047    }
1048
1049    #[test]
1050    fn test_parse_mounts() {
1051        let json = r#"{
1052            "ociVersion": "1.2.0",
1053            "mounts": [
1054                {
1055                    "destination": "/proc",
1056                    "type": "proc",
1057                    "source": "proc",
1058                    "options": ["nosuid", "noexec", "nodev"]
1059                },
1060                {
1061                    "destination": "/dev",
1062                    "type": "tmpfs",
1063                    "source": "tmpfs",
1064                    "options": ["nosuid", "strictatime", "mode=755"]
1065                }
1066            ]
1067        }"#;
1068
1069        let spec = Spec::from_json(json).unwrap();
1070        assert_eq!(spec.mounts.len(), 2);
1071        assert_eq!(spec.mounts[0].destination, "/proc");
1072        assert_eq!(spec.mounts[0].mount_type, Some("proc".to_string()));
1073        assert_eq!(spec.mounts[0].options.as_ref().unwrap().len(), 3);
1074    }
1075
1076    #[test]
1077    fn test_parse_linux_namespaces() {
1078        let json = r#"{
1079            "ociVersion": "1.2.0",
1080            "linux": {
1081                "namespaces": [
1082                    {"type": "pid"},
1083                    {"type": "network"},
1084                    {"type": "mount"},
1085                    {"type": "ipc"},
1086                    {"type": "uts"},
1087                    {"type": "user"},
1088                    {"type": "cgroup"}
1089                ]
1090            }
1091        }"#;
1092
1093        let spec = Spec::from_json(json).unwrap();
1094        let linux = spec.linux.unwrap();
1095        assert_eq!(linux.namespaces.len(), 7);
1096        assert_eq!(linux.namespaces[0].ns_type, NamespaceType::Pid);
1097        assert_eq!(linux.namespaces[1].ns_type, NamespaceType::Network);
1098    }
1099
1100    #[test]
1101    fn test_parse_linux_resources_memory() {
1102        let json = r#"{
1103            "ociVersion": "1.2.0",
1104            "linux": {
1105                "resources": {
1106                    "memory": {
1107                        "limit": 536870912,
1108                        "reservation": 268435456,
1109                        "swap": 1073741824,
1110                        "swappiness": 60
1111                    }
1112                }
1113            }
1114        }"#;
1115
1116        let spec = Spec::from_json(json).unwrap();
1117        let resources = spec.linux.unwrap().resources.unwrap();
1118        let memory = resources.memory.unwrap();
1119        assert_eq!(memory.limit, Some(536_870_912));
1120        assert_eq!(memory.reservation, Some(268_435_456));
1121        assert_eq!(memory.swap, Some(1_073_741_824));
1122        assert_eq!(memory.swappiness, Some(60));
1123    }
1124
1125    #[test]
1126    fn test_parse_linux_resources_cpu() {
1127        let json = r#"{
1128            "ociVersion": "1.2.0",
1129            "linux": {
1130                "resources": {
1131                    "cpu": {
1132                        "shares": 1024,
1133                        "quota": 100000,
1134                        "period": 100000,
1135                        "cpus": "0-3",
1136                        "mems": "0"
1137                    }
1138                }
1139            }
1140        }"#;
1141
1142        let spec = Spec::from_json(json).unwrap();
1143        let resources = spec.linux.unwrap().resources.unwrap();
1144        let cpu = resources.cpu.unwrap();
1145        assert_eq!(cpu.shares, Some(1024));
1146        assert_eq!(cpu.quota, Some(100_000));
1147        assert_eq!(cpu.period, Some(100_000));
1148        assert_eq!(cpu.cpus, Some("0-3".to_string()));
1149        assert_eq!(cpu.mems, Some("0".to_string()));
1150    }
1151
1152    #[test]
1153    fn test_parse_linux_resources_pids() {
1154        let json = r#"{
1155            "ociVersion": "1.2.0",
1156            "linux": {
1157                "resources": {
1158                    "pids": {
1159                        "limit": 1024
1160                    }
1161                }
1162            }
1163        }"#;
1164
1165        let spec = Spec::from_json(json).unwrap();
1166        let resources = spec.linux.unwrap().resources.unwrap();
1167        let pids = resources.pids.unwrap();
1168        assert_eq!(pids.limit, 1024);
1169    }
1170
1171    #[test]
1172    fn test_parse_linux_devices() {
1173        let json = r#"{
1174            "ociVersion": "1.2.0",
1175            "linux": {
1176                "devices": [
1177                    {
1178                        "type": "c",
1179                        "path": "/dev/null",
1180                        "major": 1,
1181                        "minor": 3,
1182                        "fileMode": 438,
1183                        "uid": 0,
1184                        "gid": 0
1185                    }
1186                ]
1187            }
1188        }"#;
1189
1190        let spec = Spec::from_json(json).unwrap();
1191        let linux = spec.linux.unwrap();
1192        assert_eq!(linux.devices.len(), 1);
1193        assert_eq!(linux.devices[0].device_type, "c");
1194        assert_eq!(linux.devices[0].path, "/dev/null");
1195        assert_eq!(linux.devices[0].major, Some(1));
1196        assert_eq!(linux.devices[0].minor, Some(3));
1197    }
1198
1199    #[test]
1200    fn test_parse_capabilities() {
1201        let json = r#"{
1202            "ociVersion": "1.2.0",
1203            "process": {
1204                "cwd": "/",
1205                "capabilities": {
1206                    "bounding": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
1207                    "effective": ["CAP_AUDIT_WRITE", "CAP_KILL"],
1208                    "inheritable": ["CAP_AUDIT_WRITE", "CAP_KILL"],
1209                    "permitted": ["CAP_AUDIT_WRITE", "CAP_KILL"],
1210                    "ambient": ["CAP_AUDIT_WRITE"]
1211                }
1212            }
1213        }"#;
1214
1215        let spec = Spec::from_json(json).unwrap();
1216        let caps = spec.process.unwrap().capabilities.unwrap();
1217        assert_eq!(caps.bounding.len(), 3);
1218        assert_eq!(caps.effective.len(), 2);
1219        assert_eq!(caps.inheritable.len(), 2);
1220        assert_eq!(caps.permitted.len(), 2);
1221        assert_eq!(caps.ambient.len(), 1);
1222    }
1223
1224    #[test]
1225    fn test_parse_user() {
1226        let json = r#"{
1227            "ociVersion": "1.2.0",
1228            "process": {
1229                "cwd": "/",
1230                "user": {
1231                    "uid": 1000,
1232                    "gid": 1000,
1233                    "umask": 18,
1234                    "additionalGids": [100, 200, 300]
1235                }
1236            }
1237        }"#;
1238
1239        let spec = Spec::from_json(json).unwrap();
1240        let user = spec.process.unwrap().user.unwrap();
1241        assert_eq!(user.uid, 1000);
1242        assert_eq!(user.gid, 1000);
1243        assert_eq!(user.umask, Some(18));
1244        assert_eq!(user.additional_gids, vec![100, 200, 300]);
1245    }
1246
1247    #[test]
1248    fn test_parse_rlimits() {
1249        let json = r#"{
1250            "ociVersion": "1.2.0",
1251            "process": {
1252                "cwd": "/",
1253                "rlimits": [
1254                    {
1255                        "type": "RLIMIT_NOFILE",
1256                        "soft": 1024,
1257                        "hard": 4096
1258                    },
1259                    {
1260                        "type": "RLIMIT_NPROC",
1261                        "soft": 512,
1262                        "hard": 1024
1263                    }
1264                ]
1265            }
1266        }"#;
1267
1268        let spec = Spec::from_json(json).unwrap();
1269        let rlimits = spec.process.unwrap().rlimits;
1270        assert_eq!(rlimits.len(), 2);
1271        assert_eq!(rlimits[0].rlimit_type, "RLIMIT_NOFILE");
1272        assert_eq!(rlimits[0].soft, 1024);
1273        assert_eq!(rlimits[0].hard, 4096);
1274    }
1275
1276    #[test]
1277    fn test_parse_seccomp() {
1278        let json = r#"{
1279            "ociVersion": "1.2.0",
1280            "linux": {
1281                "seccomp": {
1282                    "defaultAction": "SCMP_ACT_ERRNO",
1283                    "defaultErrnoRet": 1,
1284                    "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86"],
1285                    "syscalls": [
1286                        {
1287                            "names": ["read", "write", "exit"],
1288                            "action": "SCMP_ACT_ALLOW"
1289                        }
1290                    ]
1291                }
1292            }
1293        }"#;
1294
1295        let spec = Spec::from_json(json).unwrap();
1296        let seccomp = spec.linux.unwrap().seccomp.unwrap();
1297        assert_eq!(seccomp.default_action, "SCMP_ACT_ERRNO");
1298        assert_eq!(seccomp.default_errno_ret, Some(1));
1299        assert_eq!(seccomp.architectures.len(), 2);
1300        assert_eq!(seccomp.syscalls.len(), 1);
1301        assert_eq!(seccomp.syscalls[0].names, vec!["read", "write", "exit"]);
1302    }
1303
1304    #[test]
1305    fn test_parse_annotations() {
1306        let json = r#"{
1307            "ociVersion": "1.2.0",
1308            "annotations": {
1309                "org.opencontainers.image.os": "linux",
1310                "org.opencontainers.image.architecture": "amd64",
1311                "custom.key": "custom.value"
1312            }
1313        }"#;
1314
1315        let spec = Spec::from_json(json).unwrap();
1316        assert_eq!(spec.annotations.len(), 3);
1317        assert_eq!(
1318            spec.annotations.get("org.opencontainers.image.os"),
1319            Some(&"linux".to_string())
1320        );
1321    }
1322
1323    #[test]
1324    fn test_parse_hostname_and_domainname() {
1325        let json = r#"{
1326            "ociVersion": "1.2.0",
1327            "hostname": "test-container",
1328            "domainname": "example.com"
1329        }"#;
1330
1331        let spec = Spec::from_json(json).unwrap();
1332        assert_eq!(spec.hostname, Some("test-container".to_string()));
1333        assert_eq!(spec.domainname, Some("example.com".to_string()));
1334    }
1335
1336    #[test]
1337    fn test_parse_console_size() {
1338        let json = r#"{
1339            "ociVersion": "1.2.0",
1340            "process": {
1341                "cwd": "/",
1342                "terminal": true,
1343                "consoleSize": {
1344                    "height": 24,
1345                    "width": 80
1346                }
1347            }
1348        }"#;
1349
1350        let spec = Spec::from_json(json).unwrap();
1351        let process = spec.process.unwrap();
1352        assert!(process.terminal);
1353        let size = process.console_size.unwrap();
1354        assert_eq!(size.height, 24);
1355        assert_eq!(size.width, 80);
1356    }
1357
1358    #[test]
1359    fn test_parse_linux_masked_and_readonly_paths() {
1360        let json = r#"{
1361            "ociVersion": "1.2.0",
1362            "linux": {
1363                "maskedPaths": ["/proc/kcore", "/proc/latency_stats"],
1364                "readonlyPaths": ["/proc/sys", "/proc/sysrq-trigger"]
1365            }
1366        }"#;
1367
1368        let spec = Spec::from_json(json).unwrap();
1369        let linux = spec.linux.unwrap();
1370        assert_eq!(linux.masked_paths.len(), 2);
1371        assert_eq!(linux.readonly_paths.len(), 2);
1372        assert!(linux.masked_paths.contains(&"/proc/kcore".to_string()));
1373    }
1374
1375    #[test]
1376    fn test_parse_linux_sysctl() {
1377        let json = r#"{
1378            "ociVersion": "1.2.0",
1379            "linux": {
1380                "sysctl": {
1381                    "net.ipv4.ip_forward": "1",
1382                    "net.core.somaxconn": "1024"
1383                }
1384            }
1385        }"#;
1386
1387        let spec = Spec::from_json(json).unwrap();
1388        let linux = spec.linux.unwrap();
1389        assert_eq!(linux.sysctl.len(), 2);
1390        assert_eq!(
1391            linux.sysctl.get("net.ipv4.ip_forward"),
1392            Some(&"1".to_string())
1393        );
1394    }
1395
1396    #[test]
1397    fn test_parse_uid_gid_mappings() {
1398        let json = r#"{
1399            "ociVersion": "1.2.0",
1400            "linux": {
1401                "uidMappings": [
1402                    {"containerId": 0, "hostId": 1000, "size": 1000}
1403                ],
1404                "gidMappings": [
1405                    {"containerId": 0, "hostId": 1000, "size": 1000}
1406                ]
1407            }
1408        }"#;
1409
1410        let spec = Spec::from_json(json).unwrap();
1411        let linux = spec.linux.unwrap();
1412        assert_eq!(linux.uid_mappings.len(), 1);
1413        assert_eq!(linux.uid_mappings[0].container_id, 0);
1414        assert_eq!(linux.uid_mappings[0].host_id, 1000);
1415        assert_eq!(linux.uid_mappings[0].size, 1000);
1416    }
1417
1418    #[test]
1419    fn test_parse_block_io() {
1420        let json = r#"{
1421            "ociVersion": "1.2.0",
1422            "linux": {
1423                "resources": {
1424                    "blockIO": {
1425                        "weight": 500,
1426                        "leafWeight": 300,
1427                        "throttleReadBpsDevice": [
1428                            {"major": 8, "minor": 0, "rate": 104857600}
1429                        ],
1430                        "throttleWriteBpsDevice": [
1431                            {"major": 8, "minor": 0, "rate": 52428800}
1432                        ]
1433                    }
1434                }
1435            }
1436        }"#;
1437
1438        let spec = Spec::from_json(json).unwrap();
1439        let block_io = spec.linux.unwrap().resources.unwrap().block_io.unwrap();
1440        assert_eq!(block_io.weight, Some(500));
1441        assert_eq!(block_io.leaf_weight, Some(300));
1442        assert_eq!(block_io.throttle_read_bps_device.len(), 1);
1443        assert_eq!(block_io.throttle_read_bps_device[0].rate, 104_857_600);
1444    }
1445
1446    #[test]
1447    fn test_parse_hugepage_limits() {
1448        let json = r#"{
1449            "ociVersion": "1.2.0",
1450            "linux": {
1451                "resources": {
1452                    "hugepageLimits": [
1453                        {"pageSize": "2MB", "limit": 209715200},
1454                        {"pageSize": "1GB", "limit": 1073741824}
1455                    ]
1456                }
1457            }
1458        }"#;
1459
1460        let spec = Spec::from_json(json).unwrap();
1461        let limits = spec.linux.unwrap().resources.unwrap().hugepage_limits;
1462        assert_eq!(limits.len(), 2);
1463        assert_eq!(limits[0].page_size, "2MB");
1464        assert_eq!(limits[0].limit, 209_715_200);
1465    }
1466
1467    #[test]
1468    fn test_default_process() {
1469        let process = Process::default();
1470        assert!(!process.terminal);
1471        assert_eq!(process.cwd, "/");
1472        assert!(!process.args.is_empty());
1473        assert!(!process.env.is_empty());
1474    }
1475
1476    #[test]
1477    fn test_namespace_type_serialization() {
1478        let ns = Namespace {
1479            ns_type: NamespaceType::Network,
1480            path: Some("/var/run/netns/custom".to_string()),
1481        };
1482        let json = serde_json::to_string(&ns).unwrap();
1483        assert!(json.contains("\"type\":\"network\""));
1484        assert!(json.contains("/var/run/netns/custom"));
1485    }
1486
1487    #[test]
1488    fn test_valid_oci_versions() {
1489        // Valid semver versions should pass.
1490        for version in ["1.0.0", "1.2.0", "2.0.0-rc1", "1.0.0-alpha+build"] {
1491            let json = format!(r#"{{"ociVersion": "{version}"}}"#);
1492            assert!(
1493                Spec::from_json(&json).is_ok(),
1494                "Version {version} should be valid"
1495            );
1496        }
1497    }
1498}