Skip to main content

stree/
entry.rs

1//! Definition of a single [`Tree`] entry
2
3use std::path::Path;
4use std::fmt::{self, Display};
5use crate::{Checksum, Hash, Device, Time, Mode, FileType, Stat, Tree, util};
6use crate::{Result, error, Comparison};
7use crate::ownership::{Group, User};
8use pathstr::PathStr;
9use rhash_rs::{self as rhash, Context};
10
11#[cfg(feature = "serde")]
12use serde::{Serialize, Deserialize};
13
14#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "kebab-case"))]
16pub struct Entry
17{
18    /// The entry's file path
19    pub name: PathStr,
20    /// The type of the file
21    /// 
22    /// Depending on the file type, entries may automatically make use of (or disregard
23    /// entirely) certain fields. For example:
24    /// - [`Directories`](FileType::Directory), [`symlinks`](FileType::Symlink), [`sockets`](FileType::Socket), [`character`](FileType::Character)/[`block`](FileType::Block) special and [named pipes](FileType::Fifo)  do not yield a `size` fields
25    /// - Only files which are character/block special yield `device.special` fields
26    /// 
27    pub kind: FileType,
28    /// Time of the file's last 'modification'
29    /// 
30    /// Represents the last time the actual *content* of the file
31    /// was modified, e.g.: it was written/appended to, truncated, etc 
32    pub mtime: Time,
33    /// The file's mode/permissions
34    pub mode: Mode,
35
36    /// The file's owning user
37    pub user: User,
38
39    /// The file's owning group
40    pub group: Group,
41
42    pub size: u64,
43    /// The file's inode number
44    pub inode: Option<u64>,
45    /// Number of hard links
46    pub nlink: Option<u64>,
47    /// The device the file is on
48    pub device: Option<Device>,
49    /// The special device (character/block) if applicable
50    pub special_device: Option<Device>,
51    /// Hashes of the file
52    pub checksums: Option<Vec<Checksum>>,
53    
54    /// Time of the file's last 'change'
55    /// 
56    /// Unlike [`mtime`](Self::mtime), which tracks file content updates,
57    /// `ctime` is updated whenever permissions/ownership (`chmod`/`chown`), extended
58    /// attributes, link count (`nlink`) or the file's path/location change.
59    pub ctime: Option<Time>,
60    /// Time of the file's last access
61    pub atime: Option<Time>,
62    /// Time of the file's creation
63    pub btime: Option<Time>,
64    /// If the file is a symlink, the target of the link
65    pub link_target: Option<String>,
66    /// If the file is a symlink, the fully resolved target path
67    /// 
68    /// This is likely to point outside of the directory tree
69    pub link_real: Option<String>,
70    /// The mime type of the file
71    #[cfg(feature = "magic")]
72    pub mime: Option<String>,
73    /// The encoding of the file
74    #[cfg(feature = "magic")]
75    pub encoding: Option<String>
76}
77
78/// Builder for configuring a [`Tree`](crate::Tree) scan
79#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
80#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "kebab-case"))]
81pub struct EntryConfig
82{
83    inode: bool,
84    nlink: bool,
85    device: bool,
86    special_device: bool,
87    checksums: Option<Vec<Hash>>,
88    ctime: bool,
89    atime: bool,
90    btime: bool,
91    link_target: bool,
92    link_real: bool,
93    #[cfg(feature = "magic")]
94    mime: bool,
95    #[cfg(feature = "magic")]
96    encoding: bool
97}
98
99impl EntryConfig
100{
101    /// Create a default [`EntryConfig`]
102    pub fn new() -> Self
103    {
104        Self::default()
105    }
106    /// Enable or disable use of the `inode` field
107    pub fn inode(mut self, state: bool) -> Self
108    {
109        self.inode = state;
110        self
111    }
112    /// Enable or disable use of the `time.birth` field
113    pub fn birth_time(mut self, state: bool) -> Self
114    {
115        self.btime = state;
116        self
117    }
118    /// Enable or disable use of the `nlink` field
119    pub fn nlink(mut self, state: bool) -> Self
120    {
121        self.nlink = state;
122        self
123    }
124    /// Enable or disable use of the `time.change` field
125    pub fn change_time(mut self, state: bool) -> Self
126    {
127        self.ctime = state;
128        self
129    }
130    /// Enable or disable use of the `time.access` field
131    pub fn access_time(mut self, state: bool) -> Self
132    {
133        self.atime = state;
134        self
135    }
136    
137    /// Enable or disable use of the `device` field
138    pub fn device(mut self, state: bool) -> Self
139    {
140        self.device = state;
141        self
142    }
143    /// Enable or disable use of the `device.special` field
144    pub fn special_device(mut self, state: bool) -> Self
145    {
146        self.special_device = state;
147        self
148    }
149    /// Enable or disable use of the `hash` fields, specifying which algorithms to use
150    pub fn checksums(mut self, algos: &[Hash]) -> Self
151    {
152        self.checksums = Some(algos.to_vec());
153        self
154    }
155
156    pub fn checksum(mut self, algo: Hash) -> Self
157    {
158        match self.checksums
159        {
160            Some(ref mut cks) => cks.push(algo),
161            None => self.checksums = Some(vec![algo])
162        }
163        self
164    }
165    
166    /// Enable or disable use of the `link.real` field
167    pub fn link_real(mut self, state: bool) -> Self
168    {
169        self.link_real = state;
170        self
171    }
172    /// Enable or disable use of the `link.target` field
173    pub fn link_target(mut self, state: bool) -> Self
174    {
175        self.link_target = state;
176        self
177    }
178    /// Enable or disable use of the `mime.type` field
179    #[cfg(feature = "magic")]
180    pub fn mime_type(mut self, state: bool) -> Self
181    {
182        self.mime = state;
183        self
184    }
185    /// Enable or disable use of the `mime.encoding` field
186    #[cfg(feature = "magic")]
187    pub fn mime_encoding(mut self, state: bool) -> Self
188    {
189        self.encoding = state;
190        self
191    }
192}
193
194impl From<&Tree> for EntryConfig
195{
196    fn from(t: &Tree) -> Self
197    {
198        let mut r = Self::default();
199        for e in t.entries.iter().by_ref()
200        {
201            if e.ctime.is_some() { r.ctime = true }
202            if e.btime.is_some() { r.btime = true }
203            if e.atime.is_some() { r.atime = true }
204            if e.device.is_some() { r.device = true }
205            if e.special_device.is_some() { r.special_device = true }
206            if e.inode.is_some() { r.inode = true }
207            if e.nlink.is_some() { r.nlink = true }
208            if e.link_real.is_some() { r.link_real = true }
209            if e.link_target.is_some() { r.link_target = true }
210            if let Some(cks) = &e.checksums
211            {
212                if let Some(ref mut my_hashes) = r.checksums
213                {
214                    for ck in cks
215                    {
216                        if !my_hashes.contains(&ck.algo)
217                        {
218                            my_hashes.push(ck.algo);
219                        }
220                    }
221                }
222                else
223                {
224                    let mut hashes = Vec::with_capacity(cks.len());
225                    for ck in cks
226                    {
227                        hashes.push(ck.algo)
228                    }
229                    r.checksums = Some(hashes);
230                }
231            }
232            #[cfg(feature = "magic")]
233            if cfg!(feature = "magic")
234            {
235                if e.mime.is_some() { r.mime = true }
236            }
237            #[cfg(feature = "magic")]
238            if cfg!(feature = "magic")
239            {
240                if e.encoding.is_some() { r.encoding = true }
241            }
242        }
243        
244        r
245    }
246}
247
248impl Entry
249{
250    /// Create a new entry given a path and optionally a [`configration`](EntryConfig)
251    pub fn new<P>(file: P, conf: Option<&EntryConfig>) -> Result<Self>
252    where P: AsRef<Path>
253    {
254        let path: PathStr = file.as_ref().into();
255
256        if !path.exists()
257        && path.is_file()
258        {
259            error!("path '{path}' does not exist")
260        }
261
262        let mut r = Self::default();
263
264        r.name = path.clone();
265        let st = Stat::new(&path)?;
266        r.kind = st.filetype;
267        r.size = st.size as u64;
268        r.user.id = st.uid;
269        r.group.id = st.gid;
270        r.user.name = match util::username_from_id(st.uid)
271        {
272            Ok(uname) => Some(uname),
273            Err(_) => None
274        };
275        r.group.name = match util::groupname_from_id(st.gid)
276        {
277            Ok(gname) => Some(gname),
278            Err(_) => None
279        };
280        r.mtime = st.time.modify;
281        r.mode = Mode::from(st.mode);
282
283        if let Some(config) = conf
284        {
285            #[cfg(feature = "magic")]
286            if config.mime
287            {
288                r.mime = match util::mime(&r.name)
289                {
290                    Ok(m) => Some(m),
291                    Err(_) => Some(String::from("<error>"))
292                };
293            }
294            #[cfg(feature = "magic")]
295            if config.encoding
296            {
297                r.encoding = match util::encoding(&r.name)
298                {
299                    Ok(enc) => Some(enc),
300                    Err(_) => Some(String::from("<unknown>"))
301                };
302            }
303            if config.ctime { r.ctime = Some(st.time.change) }
304            if config.atime { r.atime = Some(st.time.access) }
305            if config.btime { r.btime = Some(crate::statx::btime(&r.name)?) }
306            if config.device
307            {
308                r.device = Some(Device::new(st.device))
309            }
310            if config.special_device
311            {
312                r.special_device = Some(Device::new(st.rdevice))
313            }
314            if config.inode
315            {
316                r.inode = Some(st.ino)
317            }
318            if config.nlink
319            {
320                r.nlink = Some(st.nlink)
321            }
322            if let Some(cksums) = &config.checksums
323            && matches!(r.kind, FileType::Regular)
324            {
325                let mut res = Vec::with_capacity(cksums.len());
326                for sum in cksums
327                {
328                    res.push(
329                        Checksum::new(sum.clone(), Context::new(rhash::Hash::from(sum.clone()))?.update_from(&r.name)?.calculate()?)
330                    );
331                }
332                r.checksums = Some(res);
333            }
334
335            if matches!(r.kind, FileType::Symlink)
336            {
337                if config.link_target
338                {
339                    if let Ok(tgt) = util::readlink(&r.name)
340                    {
341                        r.link_target = Some(tgt.string())
342                    }
343                    else
344                    {
345                        r.link_target = Some(String::from("<broken>"))
346                    }
347                }
348                if config.link_real
349                {
350                    if let Ok(real) = util::realpath(&r.name)
351                    {
352                        r.link_real = Some(real.string())
353                    }
354                    else
355                    {
356                        r.link_real = Some(String::from("<broken>"))
357                    }
358                }
359            }
360        }
361
362        Ok(r)
363    }
364
365    /// Check this entry against an on-disk file, yielding a [`Comparison`] between them
366    pub fn compare<P>(&self, path: P, conf: Option<&EntryConfig>) -> Result<Comparison>
367    where P: AsRef<Path>
368    {
369        let mut r = Comparison::default();
370        r.name = self.name.clone();
371        // let config = EntryConfig::from(self);
372        let other = Entry::new(path.as_ref(), conf)?;
373        if let Some(a) = self.btime
374        && let Some(b) = other.btime
375        && a != b
376        {
377            r.btime = Some([a, b]);
378        }
379        if let Some(a) = self.ctime
380        && let Some(b) = other.ctime
381        && a != b
382        {
383            r.ctime = Some([a, b]);
384        }
385
386        if self.mtime != other.mtime
387        {
388            r.mtime = Some([self.mtime.clone(), other.mtime.clone()]);
389        }
390        if let Some(a) = &self.checksums
391        && let Some(b) = &other.checksums
392        && a != b
393        {
394            r.checksums = Some([a.clone(), b.clone()]);
395        }
396
397        
398        if self.group.id != other.group.id
399        {
400            r.group_id = Some([self.group.id, other.group.id])
401        }
402        if self.user.id != other.user.id
403        {
404            r.user_id = Some([self.user.id, other.user.id])
405        }
406
407        if let Some(a) = &self.user.name
408        && let Some(b) = &other.user.name
409        && a != b
410        {
411            r.user_name = Some([a.clone(), b.clone()]);
412        }
413        if let Some(a) = &self.group.name
414        && let Some(b) = &other.group.name
415        && a != b
416        {
417            r.group_name = Some([a.clone(), b.clone()]);
418        }
419        
420        if let Some(a) = self.nlink
421        && let Some(b) = other.nlink
422        && a != b
423        {
424            r.nlink = Some([a, b])
425        }
426        if let Some(a) = self.device
427        && let Some(b) = other.device
428        && a != b
429        {
430            r.device = Some([a, b])
431        }
432        if let Some(a) = self.special_device
433        && let Some(b) = other.special_device
434        && a != b
435        {
436            r.special_device = Some([a, b])
437        }
438        if let Some(a) = self.inode
439        && let Some(b) = other.inode
440        && a != b
441        {
442            r.inode = Some([a, b])
443        }
444        if self.kind != other.kind
445        {
446            r.kind = Some([self.kind, other.kind])
447        }
448        if let Some(a) = &self.link_real
449        && let Some(b) = &other.link_real
450        && a != b
451        {
452            r.link_real = Some([a.clone(), b.clone()])
453        } 
454        if let Some(a) = &self.link_target
455        && let Some(b) = &other.link_target
456        && a != b
457        {
458            r.link_target = Some([a.clone(), b.clone()])
459        } 
460        #[cfg(feature = "magic")]
461        if let Some(a) = &self.mime
462        && let Some(b) = &other.mime
463        && a != b
464        {
465            r.mime_type = Some([a.clone(), b.clone()])
466        }
467        #[cfg(feature = "magic")]
468        if let Some(a) = &self.encoding
469        && let Some(b) = &other.encoding
470        && a != b
471        {
472            r.mime_encoding = Some([a.clone(), b.clone()])
473        }
474        if self.mode != other.mode
475        {
476            r.mode = Some([self.mode, other.mode])
477        }
478        Ok(r)
479    }
480}
481
482impl Display for Entry
483{
484    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
485    {
486        let _ = write!(f,
487            "{} type={} mode={} user={}:{} group={}:{} time.modify={}",
488            self.name.percent_encode(),
489            self.kind,
490            self.mode.octal(),
491            if let Some(uname) = &self.user.name { &uname } else { "<unknown>" },
492            self.user.id,
493            if let Some(gname) = &self.group.name { &gname } else { "<unknown>" },
494            self.group.id,
495            self.mtime
496        );
497        if matches!(self.kind, FileType::Regular)
498        {
499            let _ = write!(f, " size={}", self.size);
500        }
501        #[cfg(feature = "magic")]
502        if let Some(ref mime) = self.mime { let _ = write!(f, " mime.type={mime}"); }
503        #[cfg(feature = "magic")]
504        if let Some(ref enc) = self.encoding
505        && self.kind != FileType::Directory
506        {
507            let _ = write!(f, " mime.encoding={enc}");
508        }
509        
510        if let Some(ino) = self.inode { let _ = write!(f, " inode={ino}"); }
511        if let Some(nl) = self.nlink { let _ = write!(f, " nlink={nl}"); }
512        if let Some(ctime) = self.ctime { let _ = write!(f, " time.change={ctime}"); }
513        if let Some(atime) = self.atime { let _ = write!(f, " time.access={atime}"); }
514        if let Some(btime) = self.btime { let _ = write!(f, " time.birth={btime}"); }
515        if let Some(dev) = self.device { let _ = write!(f, " device={dev}"); }
516        if let Some(rdev) = self.special_device
517        && matches!(self.kind, FileType::Character|FileType::Block)
518        {
519            let _ = write!(f, " device.special={rdev}");
520        }
521        if let Some(ref cksums) = self.checksums
522        {
523            cksums.iter().for_each(|sum| { let _ = write!(f, " hash.{sum}"); });
524        }
525        if let Some(ref target) = self.link_target { let _ = write!(f, " link.target={target}"); }
526        if let Some(ref real) = self.link_real { let _ = write!(f, " link.real={real}"); }
527        Ok(())
528    }
529}