voa-core 0.4.1

File Hierarchy for the Verification of OS Artifacts (VOA)
Documentation
use std::{
    fmt::{Display, Formatter},
    str::FromStr,
};

use strum::IntoStaticStr;
use winnow::{
    ModalResult,
    Parser,
    combinator::{alt, eof},
    error::StrContext,
};

#[cfg(doc)]
use crate::identifiers::{Os, Purpose};
use crate::{
    Error,
    identifiers::{IdentifierString, SegmentPath},
};

/// A context within a [`Purpose`] for more fine-grained verifier
/// assignments.
///
/// An example for context is the name of a specific software repository when certificates are
/// used in the context of the packages purpose (e.g. "core").
///
/// If no specific context is required, [`Context::Default`] must be used.
///
/// See <https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#context>
#[derive(
    Clone, Debug, Default, strum::Display, Eq, Hash, IntoStaticStr, Ord, PartialEq, PartialOrd,
)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Context {
    /// The default context.
    #[default]
    #[strum(to_string = "default")]
    #[cfg_attr(feature = "serde", serde(rename = "default"))]
    Default,

    /// Defines a custom [`Context`] for verifiers within an [`Os`] and
    /// [`Purpose`].
    #[strum(to_string = "{0}")]
    #[cfg_attr(feature = "serde", serde(rename = "custom"))]
    Custom(CustomContext),
}

impl Context {
    /// Returns the path segment for this context.
    pub(crate) fn path_segment(&self) -> Result<SegmentPath, Error> {
        match self {
            Self::Default => SegmentPath::from_str("default"),
            Self::Custom(custom) => SegmentPath::from_str(custom.as_ref()),
        }
    }

    /// Recognizes a [`Context`] in a string slice.
    ///
    /// Consumes all of its `input`.
    ///
    /// # Errors
    ///
    /// Returns an error if
    ///
    /// - `input` does not contain a variant of [`Context`],
    /// - or one of the characters in `input` is not covered by [`IdentifierString::valid_chars`].
    ///
    /// # Examples
    ///
    /// ```
    /// use voa_core::identifiers::{Context, CustomContext};
    /// use winnow::Parser;
    ///
    /// # fn main() -> Result<(), voa_core::Error> {
    /// assert_eq!(Context::parser.parse("default")?, Context::Default);
    /// assert_eq!(
    ///     Context::parser.parse("test")?,
    ///     Context::Custom(CustomContext::new("test".parse()?))
    /// );
    /// # Ok(())
    /// # }
    /// ```
    pub fn parser(input: &mut &str) -> ModalResult<Self> {
        alt((
            ("default", eof).value(Self::Default),
            CustomContext::parser.map(Self::Custom),
        ))
        .parse_next(input)
    }
}

impl FromStr for Context {
    type Err = crate::Error;

    /// Creates a [`Context`] from a string slice.
    ///
    /// # Note
    ///
    /// Delegates to [`Context::parser`].
    ///
    /// # Errors
    ///
    /// Returns an error if [`Context::parser`] fails.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self::parser.parse(s)?)
    }
}

/// A [`CustomContext`] encodes a value for a [`Context`] that is not [`Context::Default`].
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename = "kebab-case"))]
pub struct CustomContext(IdentifierString);

impl CustomContext {
    /// Creates a new [`CustomContext`] instance.
    pub fn new(value: IdentifierString) -> Self {
        Self(value)
    }

    /// Recognizes a [`CustomContext`] in a string slice.
    ///
    /// Consumes all of its `input`.
    ///
    /// # Errors
    ///
    /// Returns an error if one of the characters in `input` is not covered by
    /// [`IdentifierString::valid_chars`].
    ///
    /// # Examples
    ///
    /// ```
    /// use voa_core::identifiers::CustomContext;
    /// use winnow::Parser;
    ///
    /// # fn main() -> Result<(), voa_core::Error> {
    /// assert_eq!(CustomContext::parser.parse("test")?.to_string(), "test");
    /// # Ok(())
    /// # }
    /// ```
    pub fn parser(input: &mut &str) -> ModalResult<Self> {
        IdentifierString::parser
            .map(Self)
            .context(StrContext::Label("custom context for VOA"))
            .parse_next(input)
    }
}

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

impl AsRef<str> for CustomContext {
    fn as_ref(&self) -> &str {
        self.0.as_ref()
    }
}

impl FromStr for CustomContext {
    type Err = crate::Error;

    /// Creates a [`CustomContext`] from a string slice.
    ///
    /// # Note
    ///
    /// Delegates to [`CustomContext::parser`].
    ///
    /// # Errors
    ///
    /// Returns an error if [`CustomContext::parser`] fails.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self::parser.parse(s)?)
    }
}

impl From<CustomContext> for Context {
    fn from(val: CustomContext) -> Self {
        Context::Custom(val)
    }
}

#[cfg(test)]
mod tests {
    use rstest::rstest;
    use testresult::TestResult;

    use super::*;

    #[rstest]
    #[case(Context::Default, "default")]
    #[case(Context::Custom(CustomContext::new("abc".parse()?)), "abc")]
    fn context_display(#[case] context: Context, #[case] display: &str) -> TestResult {
        assert_eq!(format!("{context}"), display);
        Ok(())
    }

    #[rstest]
    #[case::default("default", Context::Default)]
    #[case::custom("test", Context::Custom(CustomContext::new("test".parse()?)))]
    fn context_from_str_succeeds(#[case] input: &str, #[case] expected: Context) -> TestResult {
        assert_eq!(Context::from_str(input)?, expected);
        Ok(())
    }

    #[rstest]
    #[case::invalid_character(
        "test$",
        "test$\n    ^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
    )]
    #[case::all_caps(
        "TEST",
        "TEST\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
    )]
    #[case::empty_string(
        "",
        "\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
    )]
    fn context_from_str_fails(#[case] input: &str, #[case] error_msg: &str) -> TestResult {
        match Context::from_str(input) {
            Ok(id_string) => {
                panic!("Should have failed to parse {input} but succeeded: {id_string}");
            }
            Err(error) => {
                assert_eq!(error.to_string(), format!("Parser error:\n{error_msg}"));
                Ok(())
            }
        }
    }
}