parallel_disk_usage/hardlink/hardlink_list/
summary.rs

1use super::{iter::Item as IterItem, reflection::ReflectionEntry, HardlinkList, Reflection};
2use crate::size;
3use derive_more::{Add, AddAssign, Sum};
4use derive_setters::Setters;
5use std::{
6    cmp::Ordering,
7    fmt::{self, Display},
8};
9
10#[cfg(feature = "json")]
11use serde::{Deserialize, Serialize};
12
13/// Summary from [`HardlinkList`] or [`Reflection`].
14#[derive(Debug, Default, Setters, Clone, Copy, PartialEq, Eq, Add, AddAssign, Sum)]
15#[cfg_attr(feature = "json", derive(Deserialize, Serialize))]
16#[setters(prefix = "with_")]
17#[non_exhaustive]
18pub struct Summary<Size> {
19    /// Number of shared inodes, each with more than 1 links (i.e. `nlink > 1`).
20    pub inodes: usize,
21
22    /// Number of [shared inodes](Self::inodes) that don't have links outside the measured tree.
23    ///
24    /// This number is expected to be less than or equal to [`Self::inodes`].
25    pub exclusive_inodes: usize,
26
27    /// Totality of the numbers of links of all [shared inodes](Self::inodes).
28    pub all_links: u64,
29
30    /// Total number of links of [shared inodes](Self::inodes) that were detected within the measured tree.
31    ///
32    /// This number is expected to be less than or equal to [`Self::all_links`].
33    pub detected_links: usize,
34
35    /// Total number of links of [shared inodes](Self::inodes) that don't have links outside the measured tree.
36    ///
37    /// This number is expected to be less than or equal to [`Self::detected_links`].
38    pub exclusive_links: usize,
39
40    /// Totality of the sizes of all [shared inodes](Self::inodes).
41    pub shared_size: Size,
42
43    /// Totality of the sizes of all [shared inodes](Self::inodes) that don't have links outside the measured tree.
44    ///
45    /// This number is expected to be less than or equal to [`Self::all_links`].
46    pub exclusive_shared_size: Size,
47}
48
49/// Ability to summarize into a [`Summary`].
50pub trait SummarizeHardlinks<Size>: Sized {
51    /// The result of [`SummarizeHardlinks::summarize_hardlinks`].
52    type Summary;
53    /// Summarize into a summary of shared links and size.
54    fn summarize_hardlinks(self) -> Self::Summary;
55}
56
57/// Summary of a single unique file.
58#[derive(Debug, Clone, Copy)]
59pub struct SingleInodeSummary<Size> {
60    /// Total number of all links to the file.
61    links: u64,
62    /// Number of detected links to the file.
63    paths: usize,
64    /// Size of the file.
65    size: Size,
66}
67
68impl<Size, Iter> SummarizeHardlinks<Size> for Iter
69where
70    Size: size::Size,
71    Iter: IntoIterator,
72    Iter::Item: SummarizeHardlinks<Size>,
73    <Iter::Item as SummarizeHardlinks<Size>>::Summary: Into<SingleInodeSummary<Size>>,
74{
75    type Summary = Summary<Size>;
76    fn summarize_hardlinks(self) -> Self::Summary {
77        let mut summary = Summary::default();
78        for item in self {
79            let SingleInodeSummary { links, paths, size } = item.summarize_hardlinks().into();
80            summary.inodes += 1;
81            summary.all_links += links;
82            summary.detected_links += paths;
83            summary.shared_size += size;
84            match links.cmp(&(paths as u64)) {
85                Ordering::Greater => {}
86                Ordering::Equal => {
87                    summary.exclusive_inodes += 1;
88                    summary.exclusive_links += paths; // `links` and `paths` are both fine, but `paths` doesn't require type cast
89                    summary.exclusive_shared_size += size;
90                }
91                Ordering::Less => {
92                    panic!("Impossible! Total of nlink ({links}) is less than detected paths ({paths}). Something must have gone wrong!");
93                }
94            }
95        }
96        summary
97    }
98}
99
100impl<Size: size::Size> HardlinkList<Size> {
101    /// Create summary for the shared links and size.
102    pub fn summarize(&self) -> Summary<Size> {
103        self.iter().summarize_hardlinks()
104    }
105}
106
107impl<Size: size::Size> SummarizeHardlinks<Size> for &HardlinkList<Size> {
108    type Summary = Summary<Size>;
109    fn summarize_hardlinks(self) -> Self::Summary {
110        self.summarize()
111    }
112}
113
114impl<Size: size::Size> Reflection<Size> {
115    /// Create summary for the shared links and size.
116    pub fn summarize(&self) -> Summary<Size> {
117        self.iter().summarize_hardlinks()
118    }
119}
120
121impl<Size: size::Size> SummarizeHardlinks<Size> for &Reflection<Size> {
122    type Summary = Summary<Size>;
123    fn summarize_hardlinks(self) -> Self::Summary {
124        self.summarize()
125    }
126}
127
128/// Return type of [`Summary::display`] which implements [`Display`].
129#[derive(Debug, Clone, Copy)]
130pub struct SummaryDisplay<'a, Size: size::Size> {
131    format: Size::DisplayFormat,
132    summary: &'a Summary<Size>,
133}
134
135impl<Size: size::Size> Display for SummaryDisplay<'_, Size> {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        let SummaryDisplay { format, summary } = self;
138        let Summary {
139            inodes,
140            exclusive_inodes,
141            all_links,
142            detected_links,
143            exclusive_links,
144            shared_size,
145            exclusive_shared_size,
146        } = summary;
147
148        let shared_size = shared_size.display(*format);
149        let exclusive_shared_size = exclusive_shared_size.display(*format);
150
151        macro_rules! ln {
152            ($($args:tt)*) => {
153                writeln!(f, $($args)*)
154            };
155        }
156
157        if inodes == &0 {
158            return ln!("There are no hardlinks.");
159        }
160
161        write!(f, "Hardlinks detected! ")?;
162        if exclusive_inodes == inodes {
163            ln!("No files have links outside this tree")?;
164            ln!("* Number of shared inodes: {inodes}")?;
165            ln!("* Total number of links: {all_links}")?;
166            ln!("* Total shared size: {shared_size}")?;
167        } else if exclusive_inodes == &0 {
168            ln!("All hardlinks within this tree have links without")?;
169            ln!("* Number of shared inodes: {inodes}")?;
170            ln!("* Total number of links: {all_links} total, {detected_links} detected")?;
171            ln!("* Total shared size: {shared_size}")?;
172        } else {
173            ln!("Some files have links outside this tree")?;
174            ln!("* Number of shared inodes: {inodes} total, {exclusive_inodes} exclusive")?;
175            ln!("* Total number of links: {all_links} total, {detected_links} detected, {exclusive_links} exclusive")?;
176            ln!("* Total shared size: {shared_size} total, {exclusive_shared_size} exclusive")?;
177        }
178
179        Ok(())
180    }
181}
182
183impl<Size: size::Size> Summary<Size> {
184    /// Turns this [`Summary`] into something [displayable](Display).
185    #[inline]
186    pub fn display(&self, format: Size::DisplayFormat) -> SummaryDisplay<'_, Size> {
187        SummaryDisplay {
188            format,
189            summary: self,
190        }
191    }
192}
193
194impl<Size: Copy> SummarizeHardlinks<Size> for ReflectionEntry<Size> {
195    type Summary = SingleInodeSummary<Size>;
196    fn summarize_hardlinks(self) -> Self::Summary {
197        (&self).summarize_hardlinks()
198    }
199}
200
201impl<Size: Copy> SummarizeHardlinks<Size> for &ReflectionEntry<Size> {
202    type Summary = SingleInodeSummary<Size>;
203    fn summarize_hardlinks(self) -> Self::Summary {
204        SingleInodeSummary {
205            links: self.links,
206            paths: self.paths.len(),
207            size: self.size,
208        }
209    }
210}
211
212impl<'a, Size: Copy> SummarizeHardlinks<Size> for IterItem<'a, Size> {
213    type Summary = SingleInodeSummary<Size>;
214    fn summarize_hardlinks(self) -> Self::Summary {
215        (&self).summarize_hardlinks()
216    }
217}
218
219impl<'a, Size: Copy> SummarizeHardlinks<Size> for &IterItem<'a, Size> {
220    type Summary = SingleInodeSummary<Size>;
221    fn summarize_hardlinks(self) -> Self::Summary {
222        SingleInodeSummary {
223            links: self.links(),
224            paths: self.paths().len(),
225            size: *self.size(),
226        }
227    }
228}