use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const DEFAULT_FILE_MODE: &str = "0644";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MachineFile {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "octal")]
#[schemars(with = "Option<super::ScalarSchema>")]
pub mode: Option<String>,
}
impl MachineFile {
#[must_use]
pub fn mode_or_default(&self) -> &str {
self.mode.as_deref().unwrap_or(DEFAULT_FILE_MODE)
}
#[must_use]
pub fn sets_owner_or_mode(&self) -> bool {
self.uid.is_some() || self.gid.is_some() || self.mode.is_some()
}
}
fn octal<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
let raw: Option<serde_json::Value> = Deserialize::deserialize(d)?;
match raw {
None => Ok(None),
Some(serde_json::Value::String(s)) => Ok(Some(s)),
Some(serde_json::Value::Number(n)) => Ok(Some(n.to_string())),
Some(_) => Err(serde::de::Error::custom("`mode`: octal digits, like \"0644\"")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_mode_reads_as_text_or_number() {
let from_text: MachineFile = serde_yaml_ng::from_str("{ path: /a, content: x, mode: 0600 }").unwrap();
assert_eq!(from_text.mode.as_deref(), Some("0600"));
let value: serde_yaml_ng::Value = serde_yaml_ng::from_str("{ path: /a, content: x, mode: 0600 }").unwrap();
let from_value: MachineFile = serde_yaml_ng::from_value(value).unwrap();
assert_eq!(from_value.mode.as_deref(), Some("0600"));
for (yaml, mode) in [
("{ path: /a, content: x, mode: 600 }", "600"),
("{ path: /a, content: x, mode: \"0600\" }", "0600"),
] {
let f: MachineFile = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(f.mode_or_default(), mode, "{yaml}");
}
let unset: MachineFile = serde_yaml_ng::from_str("{ path: /a, content: x }").unwrap();
assert_eq!((unset.mode_or_default(), unset.sets_owner_or_mode()), ("0644", false));
assert!(serde_yaml_ng::from_str::<MachineFile>("{ path: /a, mode: [1] }").is_err());
}
#[test]
fn an_owner_or_a_mode_is_set_by_any_one_of_them() -> Result<(), serde_yaml_ng::Error> {
for field in ["uid: 1000", "gid: 1000", "mode: \"0600\""] {
let f: MachineFile = serde_yaml_ng::from_str(&format!("{{ path: /a, content: x, {field} }}"))?;
assert!(f.sets_owner_or_mode(), "{field}");
}
Ok(())
}
}