whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! POSIX ACL types and evaluation helpers.
//!
//! Models `system.posix_acl_access` xattr content. Mask entry limits
//! effective permissions for named user, named group, and owning group
//! entries per POSIX.1e semantics.

use serde::Serialize;

/// Single POSIX ACL entry.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AclEntry {
    /// Entry tag type.
    pub tag: AclTag,
    /// UID or GID for named user/group entries; `None` for others.
    pub qualifier: Option<u32>,
    /// Permission bits granted by this entry.
    pub perms: AclPerms,
}

/// Entry tag types per POSIX.1e.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub enum AclTag {
    /// File owner permissions (`ACL_USER_OBJ`).
    UserObj,
    /// Named user entry (`ACL_USER`).
    User,
    /// Owning group permissions (`ACL_GROUP_OBJ`).
    GroupObj,
    /// Named group entry (`ACL_GROUP`).
    Group,
    /// Mask -- limits effective perms for `User`, `GroupObj`, `Group`.
    Mask,
    /// Other (everyone else) permissions (`ACL_OTHER`).
    Other,
}

/// Permission bits for an ACL entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct AclPerms {
    /// Read permission.
    pub read: bool,
    /// Write permission.
    pub write: bool,
    /// Execute permission.
    pub execute: bool,
}

impl AclPerms {
    /// No permissions granted.
    pub const NONE: Self = Self {
        read: false,
        write: false,
        execute: false,
    };
}

/// Complete POSIX ACL (access type) for a single filesystem object.
///
/// Wraps [`AclEntry`] list with helpers for mask application,
/// named user/group queries, and effective permission computation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PosixAcl(pub(crate) Vec<AclEntry>);

impl PosixAcl {
    /// Constructs from a list of ACL entries.
    #[must_use]
    pub fn new(entries: Vec<AclEntry>) -> Self {
        Self(entries)
    }

    /// Read-only view of all entries.
    #[must_use]
    pub fn entries(&self) -> &[AclEntry] {
        &self.0
    }

    /// Mask entry's permissions, if present.
    ///
    /// Mask limits effective permissions for named user, owning group,
    /// and named group entries. if absent, those entries have full effect.
    #[must_use]
    pub fn mask(&self) -> Option<&AclPerms> {
        self.0
            .iter()
            .find(|e| e.tag == AclTag::Mask)
            .map(|e| &e.perms)
    }

    /// Finds named user ACL entry for given UID.
    #[must_use]
    pub fn named_user(&self, uid: u32) -> Option<&AclEntry> {
        self.0
            .iter()
            .find(|e| e.tag == AclTag::User && e.qualifier == Some(uid))
    }

    /// Finds named group ACL entry for given GID.
    #[must_use]
    pub fn named_group(&self, gid: u32) -> Option<&AclEntry> {
        self.0
            .iter()
            .find(|e| e.tag == AclTag::Group && e.qualifier == Some(gid))
    }

    /// `UserObj` (file owner) entry, if present.
    #[must_use]
    pub fn user_obj(&self) -> Option<&AclEntry> {
        self.0.iter().find(|e| e.tag == AclTag::UserObj)
    }

    /// `GroupObj` (owning group) entry, if present.
    #[must_use]
    pub fn group_obj(&self) -> Option<&AclEntry> {
        self.0.iter().find(|e| e.tag == AclTag::GroupObj)
    }

    /// `Other` entry, if present.
    #[must_use]
    pub fn other(&self) -> Option<&AclEntry> {
        self.0.iter().find(|e| e.tag == AclTag::Other)
    }

    /// Returns `true` if ACL has extended entries (named `User` or `Group`).
    ///
    /// Minimal ACL has only `UserObj`, `GroupObj`, `Other` (optionally `Mask`).
    /// Extended entries trigger POSIX.1e evaluation instead of mode-bit checking.
    #[must_use]
    pub fn has_extended_entries(&self) -> bool {
        self.0
            .iter()
            .any(|e| matches!(e.tag, AclTag::User | AclTag::Group))
    }

    /// All named group entries as an iterator.
    pub fn named_groups(&self) -> impl Iterator<Item = &AclEntry> {
        self.0.iter().filter(|e| e.tag == AclTag::Group)
    }

    /// Effective permissions for an entry after mask application.
    ///
    /// Per POSIX.1e: mask ANDs with permissions for `User`, `GroupObj`,
    /// and `Group` entries. `UserObj` and `Other` are unaffected.
    #[must_use]
    pub fn effective_perms(&self, entry: &AclEntry) -> AclPerms {
        let dominated_by_mask =
            matches!(entry.tag, AclTag::User | AclTag::GroupObj | AclTag::Group);
        match (dominated_by_mask, self.mask()) {
            (true, Some(mask)) => AclPerms {
                read: entry.perms.read && mask.read,
                write: entry.perms.write && mask.write,
                execute: entry.perms.execute && mask.execute,
            },
            _ => entry.perms,
        }
    }
}

#[cfg(test)]
#[path = "acl_tests.rs"]
mod tests;