isoloom_spec/model/
machine_file.rs1use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7pub const DEFAULT_FILE_MODE: &str = "0644";
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
14#[serde(deny_unknown_fields)]
15pub struct MachineFile {
16 pub path: String,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub source: Option<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub content: Option<String>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub uid: Option<u32>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub gid: Option<u32>,
31 #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "octal")]
34 #[schemars(with = "Option<super::ScalarSchema>")]
35 pub mode: Option<String>,
36}
37
38impl MachineFile {
39 #[must_use]
41 pub fn mode_or_default(&self) -> &str {
42 self.mode.as_deref().unwrap_or(DEFAULT_FILE_MODE)
43 }
44
45 #[must_use]
47 pub fn sets_owner_or_mode(&self) -> bool {
48 self.uid.is_some() || self.gid.is_some() || self.mode.is_some()
49 }
50}
51
52fn octal<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
54 let raw: Option<serde_json::Value> = Deserialize::deserialize(d)?;
55 match raw {
56 None => Ok(None),
57 Some(serde_json::Value::String(s)) => Ok(Some(s)),
58 Some(serde_json::Value::Number(n)) => Ok(Some(n.to_string())),
59 Some(_) => Err(serde::de::Error::custom("`mode`: octal digits, like \"0644\"")),
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn a_mode_reads_as_text_or_number() {
69 let from_text: MachineFile = serde_yaml_ng::from_str("{ path: /a, content: x, mode: 0600 }").unwrap();
70 assert_eq!(from_text.mode.as_deref(), Some("0600"));
71 let value: serde_yaml_ng::Value = serde_yaml_ng::from_str("{ path: /a, content: x, mode: 0600 }").unwrap();
72 let from_value: MachineFile = serde_yaml_ng::from_value(value).unwrap();
73 assert_eq!(from_value.mode.as_deref(), Some("0600"));
74 for (yaml, mode) in [
75 ("{ path: /a, content: x, mode: 600 }", "600"),
76 ("{ path: /a, content: x, mode: \"0600\" }", "0600"),
77 ] {
78 let f: MachineFile = serde_yaml_ng::from_str(yaml).unwrap();
79 assert_eq!(f.mode_or_default(), mode, "{yaml}");
80 }
81 let unset: MachineFile = serde_yaml_ng::from_str("{ path: /a, content: x }").unwrap();
82 assert_eq!((unset.mode_or_default(), unset.sets_owner_or_mode()), ("0644", false));
83 assert!(serde_yaml_ng::from_str::<MachineFile>("{ path: /a, mode: [1] }").is_err());
84 }
85
86 #[test]
87 fn an_owner_or_a_mode_is_set_by_any_one_of_them() -> Result<(), serde_yaml_ng::Error> {
88 for field in ["uid: 1000", "gid: 1000", "mode: \"0600\""] {
89 let f: MachineFile = serde_yaml_ng::from_str(&format!("{{ path: /a, content: x, {field} }}"))?;
90 assert!(f.sets_owner_or_mode(), "{field}");
91 }
92 Ok(())
93 }
94}