whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! Gathered filesystem state types.
//!
//! Complete state snapshot from OS. Check pipeline operates purely
//! on these types with zero I/O.

pub mod acl;
pub mod fsflags;
pub mod mac;
pub mod mount;
pub mod path;
pub mod subject;

use mac::MacState;
use mount::MountTable;
use path::PathComponent;
use serde::Serialize;
use subject::ResolvedSubject;

use crate::operation::Operation;

/// Tri-state for gathered data that may be unknown or inaccessible.
///
/// Distinguishes successfully gathered data, general failures,
/// and explicit permission denials during gathering. Check functions
/// treat `Unknown` and `Inaccessible` as degraded -- never false-green.
#[must_use = "probed values must be inspected -- ignoring them risks false-green results"]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "status", content = "value")]
#[non_exhaustive]
pub enum Probe<T> {
    /// Successfully gathered data.
    Known(T),
    /// Could not read -- general failure (e.g., syscall error, missing /proc).
    Unknown,
    /// Could not read -- explicit EACCES/EPERM on the probe itself.
    Inaccessible,
}

impl<T> Probe<T> {
    /// Transforms inner value, preserving `Unknown` and `Inaccessible`.
    #[must_use = "map returns a new Probe without modifying the original"]
    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Probe<U> {
        match self {
            Self::Known(v) => Probe::Known(f(v)),
            Self::Unknown => Probe::Unknown,
            Self::Inaccessible => Probe::Inaccessible,
        }
    }

    /// Reference to inner value if `Known`, `None` otherwise.
    #[must_use]
    pub fn known(&self) -> Option<&T> {
        match self {
            Self::Known(v) => Some(v),
            Self::Unknown | Self::Inaccessible => None,
        }
    }

    /// Returns `true` if this probe holds a `Known` value.
    #[must_use]
    pub fn is_known(&self) -> bool {
        matches!(self, Self::Known(_))
    }

    /// Converts `&Probe<T>` to `Probe<&T>`.
    #[must_use = "as_ref returns a new Probe without modifying the original"]
    pub fn as_ref(&self) -> Probe<&T> {
        match self {
            Self::Known(v) => Probe::Known(v),
            Self::Unknown => Probe::Unknown,
            Self::Inaccessible => Probe::Inaccessible,
        }
    }
}

/// Complete gathered state for a single permission query.
///
/// Built by gathering layer, consumed by check pipeline.
/// Contains resolved subject, path walk, mount table, and
/// requested operation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SystemState {
    /// Resolved identity performing the operation.
    pub subject: ResolvedSubject,
    /// Path components from `/` to target, each with own permissions.
    pub walk: Vec<PathComponent>,
    /// Mount entries (metadata from mountinfo, flags from `statvfs()`).
    pub mounts: MountTable,
    /// Operation being checked.
    pub operation: Operation,
    /// MAC layer state (`SELinux` and `AppArmor`), pre-gathered before pipeline runs.
    pub mac_state: MacState,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn probe_map_transforms_known_value() {
        let probe = Probe::Known(42);
        let mapped = probe.map(|v| v * 2);
        assert_eq!(mapped, Probe::Known(84));
    }

    #[test]
    fn probe_map_preserves_unknown() {
        let probe: Probe<i32> = Probe::Unknown;
        let mapped = probe.map(|v| v * 2);
        assert_eq!(mapped, Probe::<i32>::Unknown);
    }

    #[test]
    fn probe_map_preserves_inaccessible() {
        let probe: Probe<i32> = Probe::Inaccessible;
        let mapped = probe.map(|v| v * 2);
        assert_eq!(mapped, Probe::<i32>::Inaccessible);
    }

    #[test]
    fn probe_known_returns_some_for_known() {
        let probe = Probe::Known(42);
        assert_eq!(probe.known(), Some(&42));
    }

    #[test]
    fn probe_known_returns_none_for_unknown() {
        let probe: Probe<i32> = Probe::Unknown;
        assert_eq!(probe.known(), None);
    }

    #[test]
    fn probe_known_returns_none_for_inaccessible() {
        let probe: Probe<i32> = Probe::Inaccessible;
        assert_eq!(probe.known(), None);
    }

    #[test]
    fn probe_is_known_true_for_known() {
        let probe = Probe::Known(42);
        assert!(probe.is_known());
    }

    #[test]
    fn probe_is_known_false_for_unknown() {
        let probe: Probe<i32> = Probe::Unknown;
        assert!(!probe.is_known());
    }

    #[test]
    fn probe_is_known_false_for_inaccessible() {
        let probe: Probe<i32> = Probe::Inaccessible;
        assert!(!probe.is_known());
    }

    #[test]
    fn probe_as_ref_known_returns_reference() {
        let probe = Probe::Known(42);
        assert_eq!(probe.as_ref(), Probe::Known(&42));
    }

    #[test]
    fn probe_as_ref_unknown_stays_unknown() {
        let probe: Probe<i32> = Probe::Unknown;
        assert_eq!(probe.as_ref(), Probe::<&i32>::Unknown);
    }

    #[test]
    fn probe_as_ref_inaccessible_stays_inaccessible() {
        let probe: Probe<i32> = Probe::Inaccessible;
        assert_eq!(probe.as_ref(), Probe::<&i32>::Inaccessible);
    }
}