stree 0.2.0

A directory hierarchy mapping format, similar to mtree
Documentation
use std::fmt::{self, Debug, Display};
use std::str::FromStr;
use crate::{Error, Result, error, err};
use mackrow::{*};

#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};

/// Models one set of file permissions
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "kebab-case"))]
pub struct Permission
{
    /// Read permission
    pub read: bool,
    /// Write permission
    pub write: bool,
    /// Execute permission
    pub execute: bool
}

impl Permission
{
    /// Construct a single permission set from bits, will operate on
    /// [`Mode::other`] unless shifted
    pub const fn new(bits: libc::mode_t) -> Self
    {
        Self
        {
            read: bits & 0o4 != 0,
            write: bits & 0o2 != 0,
            execute: bits & 0o1 != 0
        }
    }
}

/// Fully represents the file permissions from a [`libc::mode_t`]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "kebab-case"))]
pub struct Mode
{
    /// Permissions of the file owning user
    pub owner: Permission,
    /// Permissions of the file owning group
    pub group: Permission,
    /// Permissions for users not in the owning user/group
    pub other: Permission,
    /// The 'sticky' ([`libc::S_ISVTX`]) bit is set
    pub sticky: bool,
    /// The file is setuid
    pub setuid: bool,
    /// The file is setgid
    pub setgid: bool
}

impl Mode
{
    /// Construct from a [`libc::mode_t`]
    pub const fn new(bits: libc::mode_t) -> Self
    {
        Self
        {
            owner: Permission::new(bits >> 6),
            group: Permission::new(bits >> 3),
            other: Permission::new(bits),
            setuid: bits & 0o4000 != 0,
            setgid: bits & 0o2000 != 0,
            sticky: bits & 0o1000 != 0
        }
    }

    /// Get the permission in octal format at exactly 4-digits wide;
    /// such as `0644` or `1755`
    pub fn octal(&self) -> String
    {
        format!("{:04o}", self.to_mode_t() & 0o7777)
    }

    const fn to_mode_t(&self) -> libc::mode_t
    {
        macro_rules! bitset
        {
            ($cmode:ident, $bit:expr, $state:expr) =>
            {
                if $state
                {
                    $cmode |= $bit;
                }
                else
                {
                    $cmode &= !$bit;
                }
            };
        }
        
        let mut r: libc::mode_t = 32768;
        bitset!(r, libc::S_ISVTX, self.sticky);
        bitset!(r, libc::S_ISUID, self.setuid);
        bitset!(r, libc::S_ISGID, self.setgid);
        bitset!(r, libc::S_IRUSR, self.owner.read);
        bitset!(r, libc::S_IWUSR, self.owner.write);
        bitset!(r, libc::S_IXUSR, self.owner.execute);
        bitset!(r, libc::S_IRGRP, self.group.read);
        bitset!(r, libc::S_IWGRP, self.group.write);
        bitset!(r, libc::S_IXGRP, self.group.execute);
        bitset!(r, libc::S_IROTH, self.other.read);
        bitset!(r, libc::S_IWOTH, self.other.write);
        bitset!(r, libc::S_IXOTH, self.other.execute);
        r
    }
    
    const fn from_mode_t(m: libc::mode_t) -> Self
    {
        use libc::{
            S_ISVTX, S_ISUID, S_ISGID,
            S_IRUSR, S_IWUSR, S_IXUSR,
            S_IRGRP, S_IWGRP, S_IXGRP,
            S_IROTH, S_IWOTH, S_IXOTH
        };
        
        let owner = Permission
        {
            read: (m & S_IRUSR) > 0,
            write: (m & S_IWUSR) > 0,
            execute: (m & S_IXUSR) > 0
        };
        let group = Permission
        {
            read: (m & S_IRGRP) > 0,
            write: (m & S_IWGRP) > 0,
            execute: (m & S_IXGRP) > 0
        };
        let other = Permission
        {
            read: (m & S_IROTH) > 0,
            write: (m & S_IWOTH) > 0,
            execute: (m & S_IXOTH) > 0
        };
        
        let sticky = (m & S_ISVTX) > 0;
        let setuid = (m & S_ISUID) > 0;
        let setgid = (m & S_ISGID) > 0;

        Self
        {
            owner,
            group,
            other,
            sticky,
            setuid,
            setgid
        }
    }
}

impl Display for Mode
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
    {
        let s = format!(
            "{}{}{}{}{}{}{}{}{}",
            tern!(self.owner.read    ? "r" : "-"),
            tern!(self.owner.write   ? "w" : "-"),
            tern!(self.owner.execute ? "x" : "-"),
            tern!(self.group.read    ? "r" : "-"),
            tern!(self.group.write   ? "w" : "-"),
            tern!(self.group.execute ? "x" : "-"),
            tern!(self.other.read    ? "r" : "-"),
            tern!(self.other.write   ? "w" : "-"),
            tern!(self.other.execute ? "x" : "-")
        );
        write!(f, "{s}")
    }
}

impl From<libc::mode_t> for Mode
{
    fn from(m: libc::mode_t) -> Self
    {
        Self::from_mode_t(m)
    }
}

impl From<&libc::mode_t> for Mode
{
    fn from(m: &libc::mode_t) -> Self
    {
        Self::from_mode_t(*m)
    }
}

impl From<&Mode> for libc::mode_t
{
    fn from(mode: &Mode) -> Self
    {
        mode.to_mode_t()
    }
}

impl From<Mode> for libc::mode_t
{
    fn from(mode: Mode) -> Self
    {
        mode.to_mode_t()
    }
}

impl FromStr for Mode
{
    type Err = Error;
    fn from_str(s: &str) -> Result<Self>
    {
        if s.len() != 4 { error!("mode string must consist of 4 octal digits, input is {} long", s.len()) }
        if let Err(e) = s.chars().try_for_each(|c| {
            if !c.is_numeric()
            {
                error!("mode string must consist of only numeric characters, '{c}' is not numeric")
            }
            Ok(())
        })
        {
            error!(e)
        }
        Ok(Self::from(libc::mode_t::from_str_radix(s, 8).map_err(|e| err!(e))?))
    }
}