voa_core/write.rs
1//! Traits used for writing of verifiers in VOA hierarchies.
2
3use std::{
4 fs::{File, create_dir_all},
5 io::{BufWriter, Write},
6 path::{Path, PathBuf},
7};
8
9use log::trace;
10
11use crate::{
12 Error,
13 identifiers::{Context, Os, Purpose, Technology},
14};
15
16/// A trait that provides functionality for writing a [VOA] verifier to a VOA hierarchy.
17///
18/// # Note
19///
20/// By default, [`VerifierWriter`] allows to write to arbitrary locations.
21/// While [`VerifierWriter::write_to_hierarchy`] enables the user to write a verifier to the correct
22/// location in a hierarchy, this trait does not concern itself with where that hierarchy is
23/// located, nor does it consider existing [symlinking] or [masking] rules.
24///
25/// This trait is meant to provide basic functionality for writing verifiers to a VOA hierarchy.
26/// Users of this trait must care for applying its functionality on the correct location (see [load
27/// paths]) and handle [symlinking] and [masking] separately to comply with the strict rules that
28/// [`Voa::lookup`][crate::Voa] enforces.
29///
30/// # Examples
31///
32/// ```
33/// use std::{fs::read_to_string, path::PathBuf};
34///
35/// use voa_core::{
36/// Error,
37/// VerifierWriter,
38/// identifiers::{CustomTechnology, Technology},
39/// };
40///
41/// const VERIFIER_DATA: &str = "test";
42///
43/// // A test struct implementing `VerifierWrite`
44/// struct TestWriter;
45///
46/// impl VerifierWriter for TestWriter {
47/// fn to_bytes(&self) -> Result<Vec<u8>, Error> {
48/// Ok(VERIFIER_DATA.as_bytes().to_vec())
49/// }
50///
51/// fn technology(&self) -> Technology {
52/// Technology::Custom(CustomTechnology::new("technology".parse().unwrap()))
53/// }
54///
55/// fn file_name(&self) -> PathBuf {
56/// PathBuf::from("dummy.test")
57/// }
58/// }
59///
60/// # fn main() -> testresult::TestResult {
61/// let test_dir = tempfile::tempdir()?;
62/// let path = test_dir.path();
63/// let test_writer = TestWriter;
64///
65/// // Write the verifier to a temporary directory.
66/// test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None)?;
67///
68/// // Ensure that the contents match.
69/// let verifier_file = path
70/// .join("os")
71/// .join("purpose")
72/// .join("default")
73/// .join("technology")
74/// .join(test_writer.file_name());
75/// let verifier_contents = read_to_string(verifier_file)?;
76/// assert_eq!(verifier_contents, VERIFIER_DATA);
77/// # Ok(())
78/// # }
79/// ```
80///
81/// [VOA]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/
82/// [load path]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#load-paths
83/// [symlinking]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#symlinking
84/// [masking]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#masking
85pub trait VerifierWriter {
86 /// Returns the verifier as bytes.
87 ///
88 /// # Errors
89 ///
90 /// Returns an error, if the verifier can not be returned.
91 fn to_bytes(&self) -> Result<Vec<u8>, Error>;
92
93 /// Returns the [`Technology`] used by the verifier.
94 fn technology(&self) -> Technology;
95
96 /// Returns the file name of the verifier.
97 ///
98 /// File names depend on the [`Technology`] used by the verifier.
99 fn file_name(&self) -> PathBuf;
100
101 /// Writes the verifier to a VOA hierarchy.
102 ///
103 /// The VOA hierarchy directory is provided using `path`.
104 /// Using `os`, `purpose` and `context` and the specific technology (see
105 /// [`VerifierWriter::technology`]), the correct directory for the verifier is created in
106 /// `path`. Afterwards, the verifier data is written to the specific file name (see
107 /// [`VerifierWriter::file_name`]).
108 ///
109 /// # Errors
110 ///
111 /// Returns an error if
112 ///
113 /// - the parent directory for the verifier cannot be created,
114 /// - the verifier file cannot be created,
115 /// - or the verifier data cannot be written to the file.
116 fn write_to_hierarchy(
117 &self,
118 path: impl AsRef<Path>,
119 os: Os,
120 purpose: Purpose,
121 context: Option<Context>,
122 ) -> Result<(), Error> {
123 let context = context.unwrap_or_default();
124 let path = path.as_ref();
125 let target_dir = path
126 .join(os.to_string())
127 .join(purpose.to_string())
128 .join(context.to_string())
129 .join(self.technology().to_string());
130
131 trace!("Create parent directory for verifier: {target_dir:?}");
132 create_dir_all(&target_dir).map_err(|source| Error::IoPath {
133 path: target_dir.clone(),
134 context: "creating the directory",
135 source,
136 })?;
137
138 let file_path = target_dir.join(self.file_name());
139 trace!("Write verifier data to file: {file_path:?}");
140 // Fail if the target exists, but is not a file.
141 if file_path.exists() && !file_path.is_file() {
142 return Err(Error::ExpectedFile {
143 path: file_path.to_path_buf(),
144 });
145 }
146 let mut writer =
147 BufWriter::new(File::create(&file_path).map_err(|source| Error::IoPath {
148 path: file_path.clone(),
149 context: "creating file for writing",
150 source,
151 })?);
152 writer
153 .write_all(&self.to_bytes()?)
154 .map_err(|source| Error::IoPath {
155 path: file_path,
156 context: "writing the verifier contents",
157 source,
158 })?;
159
160 Ok(())
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use std::{fs::read_to_string, os::unix::fs::symlink};
167
168 use log::debug;
169 use simplelog::{ColorChoice, Config, LevelFilter, TermLogger, TerminalMode};
170 use tempfile::tempdir;
171 use testresult::TestResult;
172
173 use super::*;
174 use crate::identifiers::CustomTechnology;
175
176 const VERIFIER_DATA: &str = "test";
177
178 struct TestWriter;
179
180 impl VerifierWriter for TestWriter {
181 fn to_bytes(&self) -> Result<Vec<u8>, Error> {
182 Ok(VERIFIER_DATA.as_bytes().to_vec())
183 }
184
185 fn technology(&self) -> Technology {
186 Technology::Custom(CustomTechnology::new("technology".parse().unwrap()))
187 }
188
189 fn file_name(&self) -> PathBuf {
190 PathBuf::from("dummy.test")
191 }
192 }
193
194 /// Init logger
195 fn init_logger() {
196 if TermLogger::init(
197 LevelFilter::Trace,
198 Config::default(),
199 TerminalMode::Stderr,
200 ColorChoice::Auto,
201 )
202 .is_err()
203 {
204 debug!("Not initializing another logger, as one is initialized already.");
205 }
206 }
207
208 /// Ensures that a verifier implementing [`VerifierWrite`] successfully writes data to a file.
209 #[test]
210 fn write_to_hierarchy_succeeds() -> TestResult {
211 init_logger();
212
213 let test_dir = tempdir()?;
214 let path = test_dir.path();
215 let test_writer = TestWriter;
216
217 test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None)?;
218
219 let target_dir = path
220 .join("os")
221 .join("purpose")
222 .join("default")
223 .join("technology");
224 assert!(target_dir.is_dir());
225
226 let verifier_file = target_dir.join(test_writer.file_name());
227 assert!(verifier_file.is_file());
228 let verifier_contents = read_to_string(verifier_file)?;
229 assert_eq!(verifier_contents, VERIFIER_DATA);
230
231 Ok(())
232 }
233
234 /// Ensures that a verifier implementing [`VerifierWrite`] fails when trying to write to a file
235 /// that is occupied by a directory.
236 #[test]
237 fn write_to_hierarchy_fails_on_target_is_dir() -> TestResult {
238 init_logger();
239
240 let test_dir = tempdir()?;
241 let path = test_dir.path();
242 let test_writer = TestWriter;
243
244 // Create a directory in place of the target file.
245 let target_file = path
246 .join("os")
247 .join("purpose")
248 .join("default")
249 .join("technology")
250 .join(test_writer.file_name());
251 create_dir_all(&target_file)?;
252 assert!(target_file.is_dir());
253
254 match test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None) {
255 Ok(()) => {
256 panic!(
257 "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
258 );
259 }
260 Err(error) => match error {
261 Error::ExpectedFile { .. } => {}
262 error => {
263 panic!("Expected Error::ExpectedFile, but got:\n{error}");
264 }
265 },
266 }
267
268 Ok(())
269 }
270
271 /// Ensures that a verifier implementing [`VerifierWrite`] fails when trying to write to a file
272 /// that is occupied by a symlink.
273 #[test]
274 fn write_to_hierarchy_fails_on_target_is_symlink() -> TestResult {
275 init_logger();
276
277 let test_dir = tempdir()?;
278 let path = test_dir.path();
279 let test_writer = TestWriter;
280
281 // Create the parent directory.
282 let parent_dir = path
283 .join("os")
284 .join("purpose")
285 .join("default")
286 .join("technology");
287 create_dir_all(&parent_dir)?;
288 let target_file = parent_dir.join(test_writer.file_name());
289 // Create a symlink to /dev/null in place of the target file.
290 symlink("/dev/null", &target_file)?;
291 assert!(target_file.is_symlink());
292
293 match test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None) {
294 Ok(()) => {
295 panic!(
296 "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
297 );
298 }
299 Err(error) => match error {
300 Error::ExpectedFile { .. } => {}
301 error => {
302 panic!("Expected Error::ExpectedFile, but got:\n{error}");
303 }
304 },
305 }
306
307 Ok(())
308 }
309
310 /// Ensures that a verifier implementing [`VerifierWrite`] fails when trying to create a
311 /// directory structure in which one element is occupied by a file.
312 #[test]
313 fn write_to_hierarchy_fails_on_dir_structure_has_file() -> TestResult {
314 init_logger();
315
316 let test_dir = tempdir()?;
317 let path = test_dir.path();
318 let test_writer = TestWriter;
319
320 // Create the parent directory.
321 let parent_dir = path.join("os").join("purpose").join("default");
322 create_dir_all(&parent_dir)?;
323
324 // Create a file in place of the last directory element.
325 let parent_file = parent_dir.join("technology");
326 let mut file = File::create(&parent_file)?;
327 file.write_all(b"Occupied!")?;
328 assert!(parent_file.is_file());
329 let target_file = parent_file.join(test_writer.file_name());
330
331 match test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None) {
332 Ok(()) => {
333 panic!(
334 "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
335 );
336 }
337 Err(error) => match error {
338 Error::IoPath { .. } => {}
339 error => {
340 panic!("Expected Error::IoPath, but got:\n{error}");
341 }
342 },
343 }
344
345 Ok(())
346 }
347
348 /// Ensures that a verifier implementing [`VerifierWrite`] fails when trying to create a
349 /// directory structure in which one element is occupied by a symlink.
350 #[test]
351 fn write_to_hierarchy_fails_on_dir_structure_has_symlink() -> TestResult {
352 init_logger();
353
354 let test_dir = tempdir()?;
355 let path = test_dir.path();
356 let test_writer = TestWriter;
357
358 // Create the parent directory.
359 let parent_dir = path.join("os").join("purpose").join("default");
360 create_dir_all(&parent_dir)?;
361
362 // Create a symlink in place of the last directory element.
363 let parent_symlink = parent_dir.join("technology");
364 symlink("/dev/null", &parent_symlink)?;
365 assert!(parent_symlink.is_symlink());
366 let target_file = parent_symlink.join(test_writer.file_name());
367
368 match test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None) {
369 Ok(()) => {
370 panic!(
371 "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
372 );
373 }
374 Err(error) => match error {
375 Error::IoPath { .. } => {}
376 error => {
377 panic!("Expected Error::IoPath, but got:\n{error}");
378 }
379 },
380 }
381
382 Ok(())
383 }
384}