1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10use crate::error::{OciError, Result};
11use crate::hooks::Hooks;
12
13pub const OCI_VERSION: &str = "1.2.0";
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct Spec {
23 pub oci_version: String,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub root: Option<Root>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub process: Option<Process>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub hostname: Option<String>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub domainname: Option<String>,
42
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub mounts: Vec<Mount>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub hooks: Option<Hooks>,
50
51 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
53 pub annotations: HashMap<String, String>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub linux: Option<Linux>,
58}
59
60impl Spec {
61 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 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 pub fn to_json(&self) -> Result<String> {
76 Ok(serde_json::to_string_pretty(self)?)
77 }
78
79 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 pub fn validate(&self) -> Result<()> {
88 if self.oci_version.is_empty() {
90 return Err(OciError::MissingField("ociVersion"));
91 }
92
93 if self.oci_version.split('.').count() < 2 {
95 return Err(OciError::InvalidVersion(self.oci_version.clone()));
96 }
97
98 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 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 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 #[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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
225pub struct Root {
226 pub path: String,
229
230 #[serde(default)]
232 pub readonly: bool,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct Process {
239 #[serde(default)]
241 pub terminal: bool,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub console_size: Option<ConsoleSize>,
246
247 pub cwd: String,
250
251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
253 pub env: Vec<String>,
254
255 #[serde(default, skip_serializing_if = "Vec::is_empty")]
257 pub args: Vec<String>,
258
259 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 pub rlimits: Vec<Rlimit>,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub apparmor_profile: Option<String>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub capabilities: Option<Capabilities>,
270
271 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
273 pub no_new_privileges: bool,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub oom_score_adj: Option<i32>,
278
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub selinux_label: Option<String>,
282
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub user: Option<User>,
286
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub io_priority: Option<IoPriority>,
290
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub scheduler: Option<Scheduler>,
294
295 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct ConsoleSize {
332 pub height: u32,
334 pub width: u32,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct Rlimit {
341 #[serde(rename = "type")]
343 pub rlimit_type: String,
344 pub soft: u64,
346 pub hard: u64,
348}
349
350#[derive(Debug, Clone, Default, Serialize, Deserialize)]
352pub struct Capabilities {
353 #[serde(default, skip_serializing_if = "Vec::is_empty")]
355 pub effective: Vec<String>,
356 #[serde(default, skip_serializing_if = "Vec::is_empty")]
358 pub bounding: Vec<String>,
359 #[serde(default, skip_serializing_if = "Vec::is_empty")]
361 pub inheritable: Vec<String>,
362 #[serde(default, skip_serializing_if = "Vec::is_empty")]
364 pub permitted: Vec<String>,
365 #[serde(default, skip_serializing_if = "Vec::is_empty")]
367 pub ambient: Vec<String>,
368}
369
370#[derive(Debug, Clone, Default, Serialize, Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub struct User {
374 #[serde(default)]
376 pub uid: u32,
377 #[serde(default)]
379 pub gid: u32,
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub umask: Option<u32>,
383 #[serde(default, skip_serializing_if = "Vec::is_empty")]
385 pub additional_gids: Vec<u32>,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct IoPriority {
391 pub class: String,
393 pub priority: i32,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct Scheduler {
400 pub policy: String,
402 #[serde(skip_serializing_if = "Option::is_none")]
404 pub nice: Option<i32>,
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub priority: Option<i32>,
408 #[serde(default, skip_serializing_if = "Vec::is_empty")]
410 pub flags: Vec<String>,
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub runtime: Option<u64>,
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub deadline: Option<u64>,
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub period: Option<u64>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct ExecCpuAffinity {
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub initial: Option<String>,
428 #[serde(rename = "final", skip_serializing_if = "Option::is_none")]
430 pub final_affinity: Option<String>,
431}
432
433#[derive(Debug, Clone, Default, Serialize, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct Mount {
437 pub destination: String,
440
441 #[serde(skip_serializing_if = "Option::is_none")]
443 pub source: Option<String>,
444
445 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
447 pub mount_type: Option<String>,
448
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub options: Option<Vec<String>>,
452
453 #[serde(default, skip_serializing_if = "Vec::is_empty")]
455 pub uid_mappings: Vec<IdMapping>,
456
457 #[serde(default, skip_serializing_if = "Vec::is_empty")]
459 pub gid_mappings: Vec<IdMapping>,
460}
461
462#[derive(Debug, Clone, Default, Serialize, Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct IdMapping {
466 pub container_id: u32,
468 pub host_id: u32,
470 pub size: u32,
472}
473
474#[derive(Debug, Clone, Default, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct Linux {
478 #[serde(default, skip_serializing_if = "Vec::is_empty")]
480 pub devices: Vec<Device>,
481
482 #[serde(default, skip_serializing_if = "Vec::is_empty")]
484 pub uid_mappings: Vec<IdMapping>,
485
486 #[serde(default, skip_serializing_if = "Vec::is_empty")]
488 pub gid_mappings: Vec<IdMapping>,
489
490 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
492 pub sysctl: HashMap<String, String>,
493
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub cgroups_path: Option<String>,
497
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub resources: Option<Resources>,
501
502 #[serde(skip_serializing_if = "Option::is_none")]
504 pub rootfs_propagation: Option<String>,
505
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub seccomp: Option<Seccomp>,
509
510 #[serde(default, skip_serializing_if = "Vec::is_empty")]
512 pub namespaces: Vec<Namespace>,
513
514 #[serde(default, skip_serializing_if = "Vec::is_empty")]
516 pub masked_paths: Vec<String>,
517
518 #[serde(default, skip_serializing_if = "Vec::is_empty")]
520 pub readonly_paths: Vec<String>,
521
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub mount_label: Option<String>,
525
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub time_offsets: Option<TimeOffsets>,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct Device {
535 #[serde(rename = "type")]
537 pub device_type: String,
538 pub path: String,
540 #[serde(skip_serializing_if = "Option::is_none")]
542 pub major: Option<i64>,
543 #[serde(skip_serializing_if = "Option::is_none")]
545 pub minor: Option<i64>,
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub file_mode: Option<u32>,
549 #[serde(skip_serializing_if = "Option::is_none")]
551 pub uid: Option<u32>,
552 #[serde(skip_serializing_if = "Option::is_none")]
554 pub gid: Option<u32>,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct Namespace {
560 #[serde(rename = "type")]
562 pub ns_type: NamespaceType,
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub path: Option<String>,
566}
567
568#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
570#[serde(rename_all = "lowercase")]
571pub enum NamespaceType {
572 Pid,
574 Network,
576 Mount,
578 Ipc,
580 Uts,
582 User,
584 Cgroup,
586 Time,
588}
589
590#[derive(Debug, Clone, Default, Serialize, Deserialize)]
592#[serde(rename_all = "camelCase")]
593pub struct Resources {
594 #[serde(default, skip_serializing_if = "Vec::is_empty")]
596 pub devices: Vec<DeviceCgroup>,
597
598 #[serde(skip_serializing_if = "Option::is_none")]
600 pub memory: Option<MemoryResources>,
601
602 #[serde(skip_serializing_if = "Option::is_none")]
604 pub cpu: Option<CpuResources>,
605
606 #[serde(rename = "blockIO", skip_serializing_if = "Option::is_none")]
608 pub block_io: Option<BlockIoResources>,
609
610 #[serde(skip_serializing_if = "Option::is_none")]
612 pub pids: Option<PidsResources>,
613
614 #[serde(default, skip_serializing_if = "Vec::is_empty")]
616 pub hugepage_limits: Vec<HugepageLimit>,
617
618 #[serde(skip_serializing_if = "Option::is_none")]
620 pub network: Option<NetworkResources>,
621
622 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
624 pub rdma: HashMap<String, RdmaResource>,
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize)]
629#[serde(rename_all = "camelCase")]
630pub struct DeviceCgroup {
631 pub allow: bool,
633 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
635 pub device_type: Option<String>,
636 #[serde(skip_serializing_if = "Option::is_none")]
638 pub major: Option<i64>,
639 #[serde(skip_serializing_if = "Option::is_none")]
641 pub minor: Option<i64>,
642 #[serde(skip_serializing_if = "Option::is_none")]
644 pub access: Option<String>,
645}
646
647#[derive(Debug, Clone, Default, Serialize, Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct MemoryResources {
651 #[serde(skip_serializing_if = "Option::is_none")]
653 pub limit: Option<i64>,
654 #[serde(skip_serializing_if = "Option::is_none")]
656 pub reservation: Option<i64>,
657 #[serde(skip_serializing_if = "Option::is_none")]
659 pub swap: Option<i64>,
660 #[serde(skip_serializing_if = "Option::is_none")]
662 pub kernel: Option<i64>,
663 #[serde(skip_serializing_if = "Option::is_none")]
665 pub kernel_tcp: Option<i64>,
666 #[serde(skip_serializing_if = "Option::is_none")]
668 pub swappiness: Option<u64>,
669 #[serde(skip_serializing_if = "Option::is_none")]
671 pub disable_oom_killer: Option<bool>,
672 #[serde(skip_serializing_if = "Option::is_none")]
674 pub use_hierarchy: Option<bool>,
675 #[serde(skip_serializing_if = "Option::is_none")]
677 pub check_before_update: Option<bool>,
678}
679
680#[derive(Debug, Clone, Default, Serialize, Deserialize)]
682#[serde(rename_all = "camelCase")]
683pub struct CpuResources {
684 #[serde(skip_serializing_if = "Option::is_none")]
686 pub shares: Option<u64>,
687 #[serde(skip_serializing_if = "Option::is_none")]
689 pub quota: Option<i64>,
690 #[serde(skip_serializing_if = "Option::is_none")]
692 pub burst: Option<u64>,
693 #[serde(skip_serializing_if = "Option::is_none")]
695 pub period: Option<u64>,
696 #[serde(skip_serializing_if = "Option::is_none")]
698 pub realtime_runtime: Option<i64>,
699 #[serde(skip_serializing_if = "Option::is_none")]
701 pub realtime_period: Option<u64>,
702 #[serde(skip_serializing_if = "Option::is_none")]
704 pub cpus: Option<String>,
705 #[serde(skip_serializing_if = "Option::is_none")]
707 pub mems: Option<String>,
708 #[serde(skip_serializing_if = "Option::is_none")]
710 pub idle: Option<i64>,
711}
712
713#[derive(Debug, Clone, Default, Serialize, Deserialize)]
715#[serde(rename_all = "camelCase")]
716pub struct BlockIoResources {
717 #[serde(skip_serializing_if = "Option::is_none")]
719 pub weight: Option<u16>,
720 #[serde(skip_serializing_if = "Option::is_none")]
722 pub leaf_weight: Option<u16>,
723 #[serde(default, skip_serializing_if = "Vec::is_empty")]
725 pub weight_device: Vec<WeightDevice>,
726 #[serde(default, skip_serializing_if = "Vec::is_empty")]
728 pub throttle_read_bps_device: Vec<ThrottleDevice>,
729 #[serde(default, skip_serializing_if = "Vec::is_empty")]
731 pub throttle_write_bps_device: Vec<ThrottleDevice>,
732 #[serde(default, skip_serializing_if = "Vec::is_empty")]
734 pub throttle_read_iops_device: Vec<ThrottleDevice>,
735 #[serde(default, skip_serializing_if = "Vec::is_empty")]
737 pub throttle_write_iops_device: Vec<ThrottleDevice>,
738}
739
740#[derive(Debug, Clone, Serialize, Deserialize)]
742#[serde(rename_all = "camelCase")]
743pub struct WeightDevice {
744 pub major: i64,
746 pub minor: i64,
748 #[serde(skip_serializing_if = "Option::is_none")]
750 pub weight: Option<u16>,
751 #[serde(skip_serializing_if = "Option::is_none")]
753 pub leaf_weight: Option<u16>,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct ThrottleDevice {
760 pub major: i64,
762 pub minor: i64,
764 pub rate: u64,
766}
767
768#[derive(Debug, Clone, Default, Serialize, Deserialize)]
770pub struct PidsResources {
771 pub limit: i64,
773}
774
775#[derive(Debug, Clone, Serialize, Deserialize)]
777#[serde(rename_all = "camelCase")]
778pub struct HugepageLimit {
779 pub page_size: String,
781 pub limit: u64,
783}
784
785#[derive(Debug, Clone, Default, Serialize, Deserialize)]
787pub struct NetworkResources {
788 #[serde(rename = "classID", skip_serializing_if = "Option::is_none")]
790 pub class_id: Option<u32>,
791 #[serde(default, skip_serializing_if = "Vec::is_empty")]
793 pub priorities: Vec<NetworkPriority>,
794}
795
796#[derive(Debug, Clone, Serialize, Deserialize)]
798pub struct NetworkPriority {
799 pub name: String,
801 pub priority: u32,
803}
804
805#[derive(Debug, Clone, Default, Serialize, Deserialize)]
807#[serde(rename_all = "camelCase")]
808pub struct RdmaResource {
809 #[serde(skip_serializing_if = "Option::is_none")]
811 pub hca_handles: Option<u32>,
812 #[serde(skip_serializing_if = "Option::is_none")]
814 pub hca_objects: Option<u32>,
815}
816
817#[derive(Debug, Clone, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820pub struct Seccomp {
821 pub default_action: String,
823 #[serde(skip_serializing_if = "Option::is_none")]
825 pub default_errno_ret: Option<u32>,
826 #[serde(default, skip_serializing_if = "Vec::is_empty")]
828 pub architectures: Vec<String>,
829 #[serde(default, skip_serializing_if = "Vec::is_empty")]
831 pub flags: Vec<String>,
832 #[serde(skip_serializing_if = "Option::is_none")]
834 pub listener_path: Option<String>,
835 #[serde(skip_serializing_if = "Option::is_none")]
837 pub listener_metadata: Option<String>,
838 #[serde(default, skip_serializing_if = "Vec::is_empty")]
840 pub syscalls: Vec<SyscallRule>,
841}
842
843#[derive(Debug, Clone, Serialize, Deserialize)]
845#[serde(rename_all = "camelCase")]
846pub struct SyscallRule {
847 pub names: Vec<String>,
849 pub action: String,
851 #[serde(skip_serializing_if = "Option::is_none")]
853 pub errno_ret: Option<u32>,
854 #[serde(default, skip_serializing_if = "Vec::is_empty")]
856 pub args: Vec<SyscallArg>,
857}
858
859#[derive(Debug, Clone, Serialize, Deserialize)]
861#[serde(rename_all = "camelCase")]
862pub struct SyscallArg {
863 pub index: u32,
865 pub value: u64,
867 #[serde(skip_serializing_if = "Option::is_none")]
869 pub value_two: Option<u64>,
870 pub op: String,
872}
873
874#[derive(Debug, Clone, Default, Serialize, Deserialize)]
876pub struct TimeOffsets {
877 #[serde(skip_serializing_if = "Option::is_none")]
879 pub monotonic: Option<TimeOffset>,
880 #[serde(skip_serializing_if = "Option::is_none")]
882 pub boottime: Option<TimeOffset>,
883}
884
885#[derive(Debug, Clone, Serialize, Deserialize)]
887pub struct TimeOffset {
888 pub secs: i64,
890 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 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}