use std::path::Path;
use std::fmt::{self, Display};
use crate::{Checksum, Hash, Device, Time, Mode, FileType, Stat, Tree, util};
use crate::{Result, error, Comparison};
use crate::ownership::{Group, User};
use pathstr::PathStr;
use rhash_rs::{self as rhash, Context};
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "kebab-case"))]
pub struct Entry
{
pub name: PathStr,
pub kind: FileType,
pub mtime: Time,
pub mode: Mode,
pub user: User,
pub group: Group,
pub size: u64,
pub inode: Option<u64>,
pub nlink: Option<u64>,
pub device: Option<Device>,
pub special_device: Option<Device>,
pub checksums: Option<Vec<Checksum>>,
pub ctime: Option<Time>,
pub atime: Option<Time>,
pub btime: Option<Time>,
pub link_target: Option<String>,
pub link_real: Option<String>,
#[cfg(feature = "magic")]
pub mime: Option<String>,
#[cfg(feature = "magic")]
pub encoding: Option<String>
}
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "kebab-case"))]
pub struct EntryConfig
{
inode: bool,
nlink: bool,
device: bool,
special_device: bool,
checksums: Option<Vec<Hash>>,
ctime: bool,
atime: bool,
btime: bool,
link_target: bool,
link_real: bool,
#[cfg(feature = "magic")]
mime: bool,
#[cfg(feature = "magic")]
encoding: bool
}
impl EntryConfig
{
pub fn new() -> Self
{
Self::default()
}
pub fn inode(mut self, state: bool) -> Self
{
self.inode = state;
self
}
pub fn birth_time(mut self, state: bool) -> Self
{
self.btime = state;
self
}
pub fn nlink(mut self, state: bool) -> Self
{
self.nlink = state;
self
}
pub fn change_time(mut self, state: bool) -> Self
{
self.ctime = state;
self
}
pub fn access_time(mut self, state: bool) -> Self
{
self.atime = state;
self
}
pub fn device(mut self, state: bool) -> Self
{
self.device = state;
self
}
pub fn special_device(mut self, state: bool) -> Self
{
self.special_device = state;
self
}
pub fn checksums(mut self, algos: &[Hash]) -> Self
{
self.checksums = Some(algos.to_vec());
self
}
pub fn checksum(mut self, algo: Hash) -> Self
{
match self.checksums
{
Some(ref mut cks) => cks.push(algo),
None => self.checksums = Some(vec![algo])
}
self
}
pub fn link_real(mut self, state: bool) -> Self
{
self.link_real = state;
self
}
pub fn link_target(mut self, state: bool) -> Self
{
self.link_target = state;
self
}
#[cfg(feature = "magic")]
pub fn mime_type(mut self, state: bool) -> Self
{
self.mime = state;
self
}
#[cfg(feature = "magic")]
pub fn mime_encoding(mut self, state: bool) -> Self
{
self.encoding = state;
self
}
}
impl From<&Tree> for EntryConfig
{
fn from(t: &Tree) -> Self
{
let mut r = Self::default();
for e in t.entries.iter().by_ref()
{
if e.ctime.is_some() { r.ctime = true }
if e.btime.is_some() { r.btime = true }
if e.atime.is_some() { r.atime = true }
if e.device.is_some() { r.device = true }
if e.special_device.is_some() { r.special_device = true }
if e.inode.is_some() { r.inode = true }
if e.nlink.is_some() { r.nlink = true }
if e.link_real.is_some() { r.link_real = true }
if e.link_target.is_some() { r.link_target = true }
if let Some(cks) = &e.checksums
{
if let Some(ref mut my_hashes) = r.checksums
{
for ck in cks
{
if !my_hashes.contains(&ck.algo)
{
my_hashes.push(ck.algo);
}
}
}
else
{
let mut hashes = Vec::with_capacity(cks.len());
for ck in cks
{
hashes.push(ck.algo)
}
r.checksums = Some(hashes);
}
}
#[cfg(feature = "magic")]
if cfg!(feature = "magic")
{
if e.mime.is_some() { r.mime = true }
}
#[cfg(feature = "magic")]
if cfg!(feature = "magic")
{
if e.encoding.is_some() { r.encoding = true }
}
}
r
}
}
impl Entry
{
pub fn new<P>(file: P, conf: Option<&EntryConfig>) -> Result<Self>
where P: AsRef<Path>
{
let path: PathStr = file.as_ref().into();
if !path.exists()
&& path.is_file()
{
error!("path '{path}' does not exist")
}
let mut r = Self::default();
r.name = path.clone();
let st = Stat::new(&path)?;
r.kind = st.filetype;
r.size = st.size as u64;
r.user.id = st.uid;
r.group.id = st.gid;
r.user.name = match util::username_from_id(st.uid)
{
Ok(uname) => Some(uname),
Err(_) => None
};
r.group.name = match util::groupname_from_id(st.gid)
{
Ok(gname) => Some(gname),
Err(_) => None
};
r.mtime = st.time.modify;
r.mode = Mode::from(st.mode);
if let Some(config) = conf
{
#[cfg(feature = "magic")]
if config.mime
{
r.mime = match util::mime(&r.name)
{
Ok(m) => Some(m),
Err(_) => Some(String::from("<error>"))
};
}
#[cfg(feature = "magic")]
if config.encoding
{
r.encoding = match util::encoding(&r.name)
{
Ok(enc) => Some(enc),
Err(_) => Some(String::from("<unknown>"))
};
}
if config.ctime { r.ctime = Some(st.time.change) }
if config.atime { r.atime = Some(st.time.access) }
if config.btime { r.btime = Some(crate::statx::btime(&r.name)?) }
if config.device
{
r.device = Some(Device::new(st.device))
}
if config.special_device
{
r.special_device = Some(Device::new(st.rdevice))
}
if config.inode
{
r.inode = Some(st.ino)
}
if config.nlink
{
r.nlink = Some(st.nlink)
}
if let Some(cksums) = &config.checksums
&& matches!(r.kind, FileType::Regular)
{
let mut res = Vec::with_capacity(cksums.len());
for sum in cksums
{
res.push(
Checksum::new(sum.clone(), Context::new(rhash::Hash::from(sum.clone()))?.update_from(&r.name)?.calculate()?)
);
}
r.checksums = Some(res);
}
if matches!(r.kind, FileType::Symlink)
{
if config.link_target
{
if let Ok(tgt) = util::readlink(&r.name)
{
r.link_target = Some(tgt.string())
}
else
{
r.link_target = Some(String::from("<broken>"))
}
}
if config.link_real
{
if let Ok(real) = util::realpath(&r.name)
{
r.link_real = Some(real.string())
}
else
{
r.link_real = Some(String::from("<broken>"))
}
}
}
}
Ok(r)
}
pub fn compare<P>(&self, path: P, conf: Option<&EntryConfig>) -> Result<Comparison>
where P: AsRef<Path>
{
let mut r = Comparison::default();
r.name = self.name.clone();
let other = Entry::new(path.as_ref(), conf)?;
if let Some(a) = self.btime
&& let Some(b) = other.btime
&& a != b
{
r.btime = Some([a, b]);
}
if let Some(a) = self.ctime
&& let Some(b) = other.ctime
&& a != b
{
r.ctime = Some([a, b]);
}
if self.mtime != other.mtime
{
r.mtime = Some([self.mtime.clone(), other.mtime.clone()]);
}
if let Some(a) = &self.checksums
&& let Some(b) = &other.checksums
&& a != b
{
r.checksums = Some([a.clone(), b.clone()]);
}
if self.group.id != other.group.id
{
r.group_id = Some([self.group.id, other.group.id])
}
if self.user.id != other.user.id
{
r.user_id = Some([self.user.id, other.user.id])
}
if let Some(a) = &self.user.name
&& let Some(b) = &other.user.name
&& a != b
{
r.user_name = Some([a.clone(), b.clone()]);
}
if let Some(a) = &self.group.name
&& let Some(b) = &other.group.name
&& a != b
{
r.group_name = Some([a.clone(), b.clone()]);
}
if let Some(a) = self.nlink
&& let Some(b) = other.nlink
&& a != b
{
r.nlink = Some([a, b])
}
if let Some(a) = self.device
&& let Some(b) = other.device
&& a != b
{
r.device = Some([a, b])
}
if let Some(a) = self.special_device
&& let Some(b) = other.special_device
&& a != b
{
r.special_device = Some([a, b])
}
if let Some(a) = self.inode
&& let Some(b) = other.inode
&& a != b
{
r.inode = Some([a, b])
}
if self.kind != other.kind
{
r.kind = Some([self.kind, other.kind])
}
if let Some(a) = &self.link_real
&& let Some(b) = &other.link_real
&& a != b
{
r.link_real = Some([a.clone(), b.clone()])
}
if let Some(a) = &self.link_target
&& let Some(b) = &other.link_target
&& a != b
{
r.link_target = Some([a.clone(), b.clone()])
}
#[cfg(feature = "magic")]
if let Some(a) = &self.mime
&& let Some(b) = &other.mime
&& a != b
{
r.mime_type = Some([a.clone(), b.clone()])
}
#[cfg(feature = "magic")]
if let Some(a) = &self.encoding
&& let Some(b) = &other.encoding
&& a != b
{
r.mime_encoding = Some([a.clone(), b.clone()])
}
if self.mode != other.mode
{
r.mode = Some([self.mode, other.mode])
}
Ok(r)
}
}
impl Display for Entry
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
{
let _ = write!(f,
"{} type={} mode={} user={}:{} group={}:{} time.modify={}",
self.name.percent_encode(),
self.kind,
self.mode.octal(),
if let Some(uname) = &self.user.name { &uname } else { "<unknown>" },
self.user.id,
if let Some(gname) = &self.group.name { &gname } else { "<unknown>" },
self.group.id,
self.mtime
);
if matches!(self.kind, FileType::Regular)
{
let _ = write!(f, " size={}", self.size);
}
#[cfg(feature = "magic")]
if let Some(ref mime) = self.mime { let _ = write!(f, " mime.type={mime}"); }
#[cfg(feature = "magic")]
if let Some(ref enc) = self.encoding
&& self.kind != FileType::Directory
{
let _ = write!(f, " mime.encoding={enc}");
}
if let Some(ino) = self.inode { let _ = write!(f, " inode={ino}"); }
if let Some(nl) = self.nlink { let _ = write!(f, " nlink={nl}"); }
if let Some(ctime) = self.ctime { let _ = write!(f, " time.change={ctime}"); }
if let Some(atime) = self.atime { let _ = write!(f, " time.access={atime}"); }
if let Some(btime) = self.btime { let _ = write!(f, " time.birth={btime}"); }
if let Some(dev) = self.device { let _ = write!(f, " device={dev}"); }
if let Some(rdev) = self.special_device
&& matches!(self.kind, FileType::Character|FileType::Block)
{
let _ = write!(f, " device.special={rdev}");
}
if let Some(ref cksums) = self.checksums
{
cksums.iter().for_each(|sum| { let _ = write!(f, " hash.{sum}"); });
}
if let Some(ref target) = self.link_target { let _ = write!(f, " link.target={target}"); }
if let Some(ref real) = self.link_real { let _ = write!(f, " link.real={real}"); }
Ok(())
}
}