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                return Err(format!(
257                    "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
258                ).into());
259            }
260            Err(error) => match error {
261                Error::ExpectedFile { .. } => {}
262                error => {
263                    return Err(format!("Expected Error::ExpectedFile, but got:\n{error}").into());
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                return Err(format!(
296                    "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
297                ).into());
298            }
299            Err(error) => match error {
300                Error::ExpectedFile { .. } => {}
301                error => {
302                    return Err(format!("Expected Error::ExpectedFile, but got:\n{error}").into());
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                return Err(format!(
334                    "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
335                ).into());
336            }
337            Err(error) => match error {
338                Error::IoPath { .. } => {}
339                error => {
340                    return Err(format!("Expected Error::IoPath, but got:\n{error}").into());
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                return Err(format!(
371                    "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
372                ).into());
373            }
374            Err(error) => match error {
375                Error::IoPath { .. } => {}
376                error => {
377                    return Err(format!("Expected Error::IoPath, but got:\n{error}").into());
378                }
379            },
380        }
381
382        Ok(())
383    }
384}