1use 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 pub name: PathStr,
20 pub kind: FileType,
28 pub mtime: Time,
33 pub mode: Mode,
35
36 pub user: User,
38
39 pub group: Group,
41
42 pub size: u64,
43 pub inode: Option<u64>,
45 pub nlink: Option<u64>,
47 pub device: Option<Device>,
49 pub special_device: Option<Device>,
51 pub checksums: Option<Vec<Checksum>>,
53
54 pub ctime: Option<Time>,
60 pub atime: Option<Time>,
62 pub btime: Option<Time>,
64 pub link_target: Option<String>,
66 pub link_real: Option<String>,
70 #[cfg(feature = "magic")]
72 pub mime: Option<String>,
73 #[cfg(feature = "magic")]
75 pub encoding: Option<String>
76}
77
78#[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 pub fn new() -> Self
103 {
104 Self::default()
105 }
106 pub fn inode(mut self, state: bool) -> Self
108 {
109 self.inode = state;
110 self
111 }
112 pub fn birth_time(mut self, state: bool) -> Self
114 {
115 self.btime = state;
116 self
117 }
118 pub fn nlink(mut self, state: bool) -> Self
120 {
121 self.nlink = state;
122 self
123 }
124 pub fn change_time(mut self, state: bool) -> Self
126 {
127 self.ctime = state;
128 self
129 }
130 pub fn access_time(mut self, state: bool) -> Self
132 {
133 self.atime = state;
134 self
135 }
136
137 pub fn device(mut self, state: bool) -> Self
139 {
140 self.device = state;
141 self
142 }
143 pub fn special_device(mut self, state: bool) -> Self
145 {
146 self.special_device = state;
147 self
148 }
149 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 pub fn link_real(mut self, state: bool) -> Self
168 {
169 self.link_real = state;
170 self
171 }
172 pub fn link_target(mut self, state: bool) -> Self
174 {
175 self.link_target = state;
176 self
177 }
178 #[cfg(feature = "magic")]
180 pub fn mime_type(mut self, state: bool) -> Self
181 {
182 self.mime = state;
183 self
184 }
185 #[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 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 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 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}