Skip to main content

exarch_core/
report.rs

1//! Extraction operation reporting.
2
3use std::path::Path;
4use std::time::Duration;
5
6/// Report of an archive extraction operation.
7///
8/// Contains statistics and metadata about the extraction process.
9#[derive(Debug, Clone, Default)]
10pub struct ExtractionReport {
11    /// Number of files successfully extracted.
12    pub files_extracted: usize,
13
14    /// Number of directories created.
15    pub directories_created: usize,
16
17    /// Number of symlinks created.
18    pub symlinks_created: usize,
19
20    /// Total bytes written to disk.
21    pub bytes_written: u64,
22
23    /// Duration of the extraction operation.
24    pub duration: Duration,
25
26    /// Number of files skipped due to security checks.
27    pub files_skipped: usize,
28
29    /// Warnings generated during extraction.
30    pub warnings: Vec<String>,
31}
32
33impl ExtractionReport {
34    /// Creates a new empty extraction report.
35    #[must_use]
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Adds a warning message to the report.
41    pub fn add_warning(&mut self, message: String) {
42        self.warnings.push(message);
43    }
44
45    /// Returns the total number of items actually written to disk
46    /// (`files_extracted + directories_created + symlinks_created`).
47    ///
48    /// This deliberately excludes [`files_skipped`](Self::files_skipped): a
49    /// report can carry meaningful progress — skipped entries, warnings —
50    /// while this is `0`. Callers deciding whether a report is worth
51    /// surfacing (e.g. `ArchiveError::partial_or`) must check
52    /// `files_skipped`/[`has_warnings`](Self::has_warnings) too, not rely on
53    /// this alone.
54    #[must_use]
55    pub fn total_items(&self) -> usize {
56        self.files_extracted + self.directories_created + self.symlinks_created
57    }
58
59    /// Returns whether any warnings were generated.
60    #[must_use]
61    pub fn has_warnings(&self) -> bool {
62        !self.warnings.is_empty()
63    }
64}
65
66/// Callback trait for progress reporting during archive operations.
67///
68/// Implement this trait to receive progress updates during extraction or
69/// creation. The trait requires `Send` to allow use in multi-threaded contexts.
70///
71/// # Examples
72///
73/// ```
74/// use exarch_core::ProgressCallback;
75/// use std::path::Path;
76///
77/// struct SimpleProgress;
78///
79/// impl ProgressCallback for SimpleProgress {
80///     fn on_entry_start(&mut self, path: &Path, total: usize, current: usize) {
81///         println!("Processing {}/{}: {}", current, total, path.display());
82///     }
83///
84///     fn on_bytes_written(&mut self, bytes: u64) {
85///         // Track bytes written
86///     }
87///
88///     fn on_entry_complete(&mut self, path: &Path) {
89///         println!("Completed: {}", path.display());
90///     }
91///
92///     fn on_complete(&mut self) {
93///         println!("Operation complete");
94///     }
95/// }
96/// ```
97pub trait ProgressCallback: Send {
98    /// Called when starting to process an entry.
99    ///
100    /// # Arguments
101    ///
102    /// * `path` - Path of the entry being processed
103    /// * `total` - Total number of entries in the archive
104    /// * `current` - Current entry number (1-indexed)
105    fn on_entry_start(&mut self, path: &Path, total: usize, current: usize);
106
107    /// Called when bytes are written during extraction or read during creation.
108    ///
109    /// Granularity is per-entry: called once after the full entry is written.
110    /// Partial writes on failure are not reported. Not called for entries that
111    /// produce no output (directories, skipped entries).
112    ///
113    /// # Arguments
114    ///
115    /// * `bytes` - Number of bytes written/read in this update
116    fn on_bytes_written(&mut self, bytes: u64);
117
118    /// Called when an entry has been completely processed.
119    ///
120    /// Always called after [`on_entry_start`](Self::on_entry_start) for the
121    /// same entry, including when extraction of that entry fails. Callers can
122    /// rely on this pairing for cleanup or progress accounting.
123    ///
124    /// # Arguments
125    ///
126    /// * `path` - Path of the entry that was completed
127    fn on_entry_complete(&mut self, path: &Path);
128
129    /// Called when the entire operation completes successfully.
130    ///
131    /// Not called if the operation fails or results in a partial extraction.
132    /// Implementors must not rely on this method for cleanup — use `Drop`
133    /// instead.
134    fn on_complete(&mut self);
135}
136
137/// No-op implementation of `ProgressCallback` that does nothing.
138///
139/// Use this when you don't need progress reporting but the API requires
140/// a callback implementation.
141#[derive(Debug, Default)]
142pub struct NoopProgress;
143
144impl ProgressCallback for NoopProgress {
145    fn on_entry_start(&mut self, _path: &Path, _total: usize, _current: usize) {}
146
147    fn on_bytes_written(&mut self, _bytes: u64) {}
148
149    fn on_entry_complete(&mut self, _path: &Path) {}
150
151    fn on_complete(&mut self) {}
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_new_report() {
160        let report = ExtractionReport::new();
161        assert_eq!(report.files_extracted, 0);
162        assert_eq!(report.directories_created, 0);
163        assert_eq!(report.bytes_written, 0);
164        assert!(!report.has_warnings());
165    }
166
167    #[test]
168    fn test_add_warning() {
169        let mut report = ExtractionReport::new();
170        report.add_warning("Test warning".to_string());
171        assert!(report.has_warnings());
172        assert_eq!(report.warnings.len(), 1);
173    }
174
175    #[test]
176    fn test_total_items() {
177        let mut report = ExtractionReport::new();
178        report.files_extracted = 10;
179        report.directories_created = 5;
180        report.symlinks_created = 2;
181        assert_eq!(report.total_items(), 17);
182    }
183}