stree 0.2.0

A directory hierarchy mapping format, similar to mtree
Documentation
use crate::{Time, FileType, Mode, Checksum, Device};
use pathstr::PathStr;
use std::fmt::{self, Display};

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

/// Represents a comparison between a two [`Tree`](crate::Tree) instances
///
/// In the field descriptions, the trees are referred to as subject and object. Since
/// this struct is returned from methods belonging to the [`Tree`](crate::Tree) struct, the tree
/// performing the method call is the subject, and the object is the tree being compared against
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "kebab-case"))]
pub struct Comparisons
{
    /// The list of comparisons between the trees
    pub inner: Vec<Comparison>,
    /// A list of paths which were *not* in the subject tree, but *were* present in the object tree
    pub aliens: Option<Vec<PathStr>>,
    /// A list of paths which *were* in the subject tree, but were *not* present in the object tree
    pub missing: Option<Vec<PathStr>>
}

/// Represents a comparison between an [`Entry`](crate::Entry) and a file on-disk
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "kebab-case"))]
pub struct Comparison
{
    /// The file's path
    pub name: PathStr,
    /// Difference between their `time.modify`
    pub mtime: Option<[Time; 2]>,
    /// Difference between their `time.birth`
    pub btime: Option<[Time; 2]>,
    /// Difference between their `time.change`
    pub ctime: Option<[Time; 2]>,
    /// Difference between their `nlink` values
    pub nlink: Option<[libc::nlink_t; 2]>,
    /// Difference between their `inode`
    pub inode: Option<[libc::ino_t; 2]>,
    /// Difference between their owning user names, the first half of the `user` field
    pub user_name: Option<[String; 2]>,
    /// Difference between their owning group names, the first half of the `group` field
    pub group_name: Option<[String; 2]>,
    /// Difference between their owning user UID, the second half of the `user` field
    pub user_id: Option<[libc::uid_t; 2]>,
    /// Difference between their owning group GID, the second half of the `group` field
    pub group_id: Option<[libc::gid_t; 2]>,
    /// Difference between their `type`
    pub kind: Option<[FileType; 2]>,
    /// Difference between their `size`
    pub size: Option<[u64; 2]>,
    /// Difference between their `mode`
    pub mode: Option<[Mode; 2]>,
    /// Difference between their `mime.type`
    #[cfg(feature = "magic")]
    pub mime_type: Option<[String; 2]>,
    /// Difference between their `mime.encoding`
    #[cfg(feature = "magic")]
    pub mime_encoding: Option<[String; 2]>,
    /// Difference between their `device`
    pub device: Option<[Device; 2]>,
    /// Difference between their `device.special`
    pub special_device: Option<[Device; 2]>,
    /// Difference between their `link.target`
    pub link_target: Option<[String; 2]>,
    /// Difference between their `link.real`
    pub link_real: Option<[String; 2]>,
    /// Differences between their checksums/hashes
    pub checksums: Option<[Vec<Checksum>; 2]>
}

impl Comparison
{
    /// Returns true if none of the values in the comparison are [`Some`], i.e.:
    /// there is nothing to compare
    pub fn is_empty(&self) -> bool
    {
        #[cfg(feature = "magic")]
        let mgc = self.mime_type.is_none() && self.mime_encoding.is_none();
        #[cfg(not(feature = "magic"))]
        let mgc = true;
        mgc
        && self.mtime.is_none()
        && self.btime.is_none()
        && self.ctime.is_none()
        && self.nlink.is_none()
        && self.inode.is_none()
        && self.user_name.is_none()
        && self.group_name.is_none()
        && self.user_id.is_none()
        && self.group_id.is_none()
        && self.kind.is_none()
        && self.size.is_none()
        && self.mode.is_none()
        && self.device.is_none()
        && self.special_device.is_none()
        && self.link_target.is_none()
        && self.link_real.is_none()
        && self.checksums.is_none()
    }
}

impl Display for Comparison
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
    {
        if self.is_empty() { return Ok(()) }
        writeln!(f, "PATH: \u{1B}[1m{}\u{1B}[m", self.name)?;
        if let Some(x) = self.mtime
        {
            writeln!(f, "time.modify: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.btime
        {
            writeln!(f, "time.birth: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.ctime
        {
           writeln!(f, "time.change: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.nlink
        {
           writeln!(f, "nlink: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.inode
        {
           writeln!(f, "inode: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = &self.user_name
        {
           writeln!(f, "user.name: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.user_id
        {
           writeln!(f, "user.id: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = &self.group_name
        {
           writeln!(f, "group.name: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.group_id
        {
           writeln!(f, "group.id: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.kind
        {
           writeln!(f, "type: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.mode
        {
           writeln!(f, "mode: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.size
        {
           writeln!(f, "size: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.device
        {
           writeln!(f, "device: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = self.special_device
        {
           writeln!(f, "device.special: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = &self.link_target
        {
           writeln!(f, "link.target: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(x) = &self.link_real
        {
           writeln!(f, "link.real: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        #[cfg(feature = "magic")]
        if let Some(x) = &self.mime_type
        {
            writeln!(f, "mime.type: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        #[cfg(feature = "magic")]
        if let Some(x) = &self.mime_encoding
        {
            writeln!(f, "mime.encoding: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", x[0], x[1])?;
        }
        if let Some(cks) = &self.checksums
        {
            for n in 0..cks[0].len()
            {
                let a = &cks[0][n];
                let b = &cks[1][n];
                writeln!(f, "hash.{}: \u{1B}[32m{}\u{1B}[m != \u{1B}[31m{}\u{1B}[m", a.algo, a.hash, b.hash)?;
            }
        }
        Ok(())
    }
}

impl Display for Comparisons
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
    {
        if let Some(missing) = &self.missing
        && !missing.is_empty()
        {
            writeln!(f, "Files \u{1B}[1;3mexpected\u{1B}[m but not present:")?;
            for miss in missing.iter()
            {
                writeln!(f, "- \u{1B}[31m{miss}\u{1B}[m")?;
            }
            writeln!(f, "")?;
        }
        if let Some(aliens) = &self.aliens
        && !aliens.is_empty()
        {
            writeln!(f, "Files \u{1B}[1;3mpresent\u{1B}[m but not expected:")?;
            for alien in aliens.iter()
            {
                writeln!(f, "- \u{1B}[33m{alien}\u{1B}[m")?;
            }
            writeln!(f, "")?;
        }
        if !self.inner.is_empty()
        {
            self.inner.iter().try_for_each(|e| writeln!(f, "{e}"))?;
            writeln!(f, "\u{1B}[33;1mwarn\u{1B}[m: {} differences found in expected files", self.inner.len())?;
        }
        else
        {
            writeln!(f, "\u{1B}[32;1mok\u{1B}[m: 0 differences found in expected files")?;
        }

        Ok(())
    }
}