voa 0.7.3

Command line interface and library for interacting with the File Hierarchy for the Verification of OS Artifacts (VOA)
Documentation
//! Utilities for library and CLI.

use std::{
    fmt::Display,
    path::{Path, PathBuf},
    str::FromStr,
};

use voa_core::identifiers::Purpose;

use crate::Error;

/// Directory or regular file.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum DirOrFileType {
    /// A directory.
    Dir,
    /// A regular file.
    File,
}

/// A path that is guaranteed to be a directory or regular file.
///
/// Wraps a [`PathBuf`] and a [`DirOrFileType`] which indicates whether a directory or regular file
/// is targeted.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DirOrFile {
    path: PathBuf,
    /// The type of path (either a directory or a regular file).
    pub typ: DirOrFileType,
}

impl AsRef<Path> for DirOrFile {
    fn as_ref(&self) -> &Path {
        &self.path
    }
}

impl TryFrom<PathBuf> for DirOrFile {
    type Error = Error;

    /// Creates a [`DirOrFile`] from [`PathBuf`].
    ///
    /// # Errors
    ///
    /// Returns an error if `value` represents neither a directory, nor a regular file.
    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
        if value.is_dir() {
            Ok(Self {
                path: value,
                typ: DirOrFileType::Dir,
            })
        } else if value.is_file() {
            Ok(Self {
                path: value,
                typ: DirOrFileType::File,
            })
        } else {
            Err(crate::Error::PathIsNotDirOrFile { path: value })
        }
    }
}

impl TryFrom<&Path> for DirOrFile {
    type Error = Error;

    /// Creates a [`DirOrFile`] from [`Path`] reference.
    ///
    /// # Errors
    ///
    /// Returns an error if `value` represents neither a directory, nor a regular file.
    fn try_from(value: &Path) -> Result<Self, Self::Error> {
        Self::try_from(value.to_path_buf())
    }
}

impl FromStr for DirOrFile {
    type Err = Error;

    /// Creates a [`DirOrFile`] from a string slice.
    ///
    /// # Errors
    ///
    /// Returns an error if `value` represents neither a directory, nor a regular file.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::try_from(PathBuf::from(s))
    }
}

/// A path that is guaranteed to be a regular file.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct RegularFile(PathBuf);

impl AsRef<Path> for RegularFile {
    /// Returns a reference to the wrapped path.
    fn as_ref(&self) -> &Path {
        &self.0
    }
}

impl TryFrom<PathBuf> for RegularFile {
    type Error = Error;

    /// Creates a [`RegularFile`] from a [`PathBuf`].
    ///
    /// # Errors
    ///
    /// Returns an error if `value` does not represent a regular file.
    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
        if !value.is_file() {
            return Err(Error::PathIsNotAFile { path: value });
        }

        Ok(Self(value))
    }
}

impl FromStr for RegularFile {
    type Err = Error;

    /// Creates a [`RegularFile`] from a string slice.
    ///
    /// # Errors
    ///
    /// Returns an error if `s` does not represent a regular file.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::try_from(PathBuf::from(s))
    }
}

/// A wrapper for a [`Purpose`] that is guaranteed to be for an artifact verifier.
#[derive(Clone, Debug)]
pub struct ArtifactVerifierPurpose(Purpose);

impl ArtifactVerifierPurpose {
    /// Creates a new [`ArtifactVerifierPurpose`].
    ///
    /// # Errors
    ///
    /// Returns an error if the provided `purpose` represents the trust anchor mode.
    pub fn new(purpose: Purpose) -> Result<Self, Error> {
        if purpose.is_trust_anchor() {
            return Err(Error::PurposeIsATrustAnchor { purpose });
        }

        Ok(Self(purpose))
    }
}

impl AsRef<Purpose> for ArtifactVerifierPurpose {
    /// Returns a reference to the wrapped [`Purpose`].
    fn as_ref(&self) -> &Purpose {
        &self.0
    }
}

impl Display for ArtifactVerifierPurpose {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl FromStr for ArtifactVerifierPurpose {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::new(Purpose::from_str(s)?)
    }
}

#[cfg(test)]
mod tests {
    use tempfile::{NamedTempFile, TempDir};
    use testresult::TestResult;

    use super::*;

    #[test]
    fn dir_or_file_from_str_is_dir() -> TestResult {
        let temp = TempDir::new()?;
        let Some(path) = temp.path().to_str() else {
            panic!("Could not convert temporary dir to string slice");
        };

        let dir = DirOrFile::from_str(path)?;
        assert_eq!(dir.typ, DirOrFileType::Dir);
        assert_eq!(dir.as_ref(), temp.path());
        Ok(())
    }

    #[test]
    fn dir_or_file_from_str_is_file() -> TestResult {
        let temp = NamedTempFile::new()?;
        let Some(path) = temp.path().to_str() else {
            panic!("Could not convert temporary dir to string slice");
        };

        let dir = DirOrFile::from_str(path)?;
        assert_eq!(dir.typ, DirOrFileType::File);
        assert_eq!(dir.as_ref(), temp.path());
        Ok(())
    }

    #[test]
    #[cfg(target_os = "linux")]
    fn dir_or_file_from_str_fails_on_not_a_dir_or_a_file() -> TestResult {
        let result = DirOrFile::from_str("/dev/urandom");
        match result {
            Ok(path) => {
                panic!("Succeeded to create a DirOrFile from {path:?} but should have failed");
            }
            Err(Error::PathIsNotDirOrFile { .. }) => {}
            Err(error) => {
                panic!("Should have returned Error::PathIsNotDirOrFile, but returned: {error}");
            }
        }
        Ok(())
    }

    #[test]
    fn regular_file_from_str_succeeds() -> TestResult {
        let temp = NamedTempFile::new()?;
        let Some(path) = temp.path().to_str() else {
            panic!("Could not convert temporary dir to string slice");
        };

        let file = RegularFile::from_str(path)?;
        assert_eq!(file.as_ref(), temp.path());
        Ok(())
    }

    #[test]
    fn regular_file_from_str_fails_on_dir() -> TestResult {
        let temp = TempDir::new()?;
        let Some(path) = temp.path().to_str() else {
            panic!("Could not convert temporary dir to string slice");
        };

        let result = RegularFile::from_str(path);
        match result {
            Ok(path) => {
                panic!("Succeeded to create a RegularFile from {path:?} but should have failed");
            }
            Err(Error::PathIsNotAFile { .. }) => {}
            Err(error) => {
                panic!("Should have returned Error::PathIsNotAFile, but returned: {error}");
            }
        }
        Ok(())
    }

    #[test]
    fn artifact_verifier_purpose_from_str_succeeds() -> TestResult {
        let artifact_verifier_purpose = ArtifactVerifierPurpose::from_str("some-purpose")?;
        let _purpose = artifact_verifier_purpose.as_ref();
        println!("{artifact_verifier_purpose}");

        Ok(())
    }

    #[test]
    fn artifact_verifier_purpose_from_str_fails() -> TestResult {
        match ArtifactVerifierPurpose::from_str("trust-anchor-some-purpose") {
            Err(Error::PurposeIsATrustAnchor { .. }) => {}
            Ok(purpose) => panic!(
                "Should have failed with Error::PurposeIsATrustAnchor but succeeded instead: {purpose}"
            ),
            Err(error) => panic!(
                "Should have failed with Error::PurposeIsATrustAnchor but failed differently: {error}"
            ),
        }
        Ok(())
    }
}