1use std::fmt::{self, Display, Formatter};
7use std::str::FromStr;
8
9use schemars::{JsonSchema, json_schema};
10use serde::{Deserialize, Deserializer, Serialize};
11
12use super::names::{InputName, Text};
13use crate::Error;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum FileSource {
18 Source(String),
20 Content(String),
22 Input(InputName),
24 Secret(String),
26}
27
28impl FileSource {
29 #[must_use]
31 pub fn is_value(&self) -> bool {
32 matches!(self, FileSource::Input(_) | FileSource::Secret(_))
33 }
34
35 #[must_use]
37 pub fn key(&self) -> &'static str {
38 match self {
39 FileSource::Source(_) => "source",
40 FileSource::Content(_) => "content",
41 FileSource::Input(_) => "input",
42 FileSource::Secret(_) => "secret",
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct Mode(u32);
50
51impl Mode {
52 pub const CONFIG: Mode = Mode(0o644);
54 pub const VALUE: Mode = Mode(0o400);
56
57 #[must_use]
59 pub fn bits(self) -> u32 {
60 self.0
61 }
62}
63
64impl FromStr for Mode {
65 type Err = Error;
66
67 fn from_str(digits: &str) -> Result<Mode, Error> {
73 u32::from_str_radix(digits, 8)
74 .ok()
75 .filter(|m| *m <= 0o777)
76 .map(Mode)
77 .ok_or_else(|| Error::Mode(digits.to_owned()))
78 }
79}
80
81impl Display for Mode {
82 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
83 write!(f, "{:04o}", self.0)
84 }
85}
86
87impl Serialize for Mode {
88 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
90 s.collect_str(self)
91 }
92}
93
94impl<'de> Deserialize<'de> for Mode {
95 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Mode, D::Error> {
97 d.deserialize_any(Text("octal digits, like \"0444\""))?
98 .parse()
99 .map_err(serde::de::Error::custom)
100 }
101}
102
103impl JsonSchema for Mode {
104 fn schema_name() -> std::borrow::Cow<'static, str> {
105 "Mode".into()
106 }
107
108 fn inline_schema() -> bool {
109 true
110 }
111
112 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
113 json_schema!({ "type": ["string", "integer"], "pattern": "^0?[0-7]{1,3}$" })
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
119#[serde(try_from = "FileInput")]
120pub struct File {
121 pub path: String,
123 pub source: FileSource,
125 pub uid: Option<u32>,
127 pub gid: Option<u32>,
129 pub mode: Option<Mode>,
131}
132
133impl File {
134 #[must_use]
136 pub fn mode_or_default(&self) -> Mode {
137 self.mode.unwrap_or(if self.source.is_value() { Mode::VALUE } else { Mode::CONFIG })
138 }
139}
140
141#[derive(Deserialize, JsonSchema)]
143#[serde(deny_unknown_fields)]
144struct FileInput {
145 path: String,
148 #[serde(default)]
151 source: Option<String>,
152 #[serde(default)]
154 content: Option<String>,
155 #[serde(default)]
158 input: Option<InputName>,
159 #[serde(default)]
161 secret: Option<String>,
162 #[serde(default)]
164 uid: Option<u32>,
165 #[serde(default)]
167 gid: Option<u32>,
168 #[serde(default)]
172 mode: Option<Mode>,
173}
174
175impl TryFrom<FileInput> for File {
176 type Error = Error;
177
178 fn try_from(f: FileInput) -> Result<File, Error> {
179 let given: Vec<FileSource> = [
180 f.source.map(FileSource::Source),
181 f.content.map(FileSource::Content),
182 f.input.map(FileSource::Input),
183 f.secret.map(FileSource::Secret),
184 ]
185 .into_iter()
186 .flatten()
187 .collect();
188 let source = match <[FileSource; 1]>::try_from(given) {
189 Ok([one]) => one,
190 Err(given) if given.is_empty() => return Err(Error::NoFileSource),
191 Err(given) => return Err(Error::FileSources(given.iter().map(FileSource::key).collect())),
192 };
193 Ok(File {
194 path: f.path,
195 source,
196 uid: f.uid,
197 gid: f.gid,
198 mode: f.mode,
199 })
200 }
201}
202
203impl JsonSchema for File {
204 fn schema_name() -> std::borrow::Cow<'static, str> {
205 "File".into()
206 }
207
208 fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
209 FileInput::json_schema(g)
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn a_file_has_one_source_and_a_mode_by_kind() {
219 let value: File = serde_yaml_ng::from_str("{ path: /ctf/flag, input: FLAG }").unwrap();
220 assert!(value.source.is_value());
221 assert_eq!(value.mode_or_default().to_string(), "0400");
222 let widened: File = serde_yaml_ng::from_str("{ path: /a, content: x, mode: 0444 }").unwrap();
223 assert_eq!(widened.mode_or_default(), Mode(0o444));
224 let config: File = serde_yaml_ng::from_str("{ path: /a, source: a.ini }").unwrap();
225 assert_eq!(config.mode_or_default(), Mode::CONFIG);
226 }
227
228 #[test]
229 fn a_mode_is_octal_text_or_the_number_yaml_reads() {
230 assert_eq!(serde_yaml_ng::from_str::<Mode>("\"0444\"").map(Mode::bits).ok(), Some(0o444));
231 assert_eq!(serde_yaml_ng::from_str::<Mode>("640").map(Mode::bits).ok(), Some(0o640));
232 assert_eq!(serde_json::to_string(&Mode::VALUE).ok().as_deref(), Some("\"0400\""));
233 let wrong = serde_yaml_ng::from_str::<Mode>("[4]").unwrap_err().to_string();
234 assert!(wrong.contains("octal digits, like \"0444\""), "{wrong}");
235 }
236
237 #[test]
238 fn a_file_with_two_sources_or_none_is_refused() {
239 let two = serde_yaml_ng::from_str::<File>("{ path: /a, source: a, input: FLAG }").unwrap_err().to_string();
240 assert!(two.contains("`source` and `input`"), "{two}");
241 let none = serde_yaml_ng::from_str::<File>("{ path: /a }").unwrap_err().to_string();
242 assert!(none.contains("needs one of"), "{none}");
243 assert!(serde_yaml_ng::from_str::<File>("{ path: /a, content: x, mode: 0999 }").is_err());
244 }
245}