pacsea/logic/
summary.rs

1//! Post-transaction summary computation for file changes and service impacts.
2
3use crate::state::PackageItem;
4
5/// What: Minimal data required to populate the `PostSummary` modal.
6///
7/// Inputs:
8/// - Populated by `compute_post_summary` after pacman inspections.
9///
10/// Output:
11/// - Supplies boolean outcome, counts, and auxiliary labels for post-transaction display.
12///
13/// Details:
14/// - Contains information about what changed during the package operation.
15/// - Designed to be serializable/clonable so the UI can render snapshots outside the logic module.
16#[derive(Debug, Clone)]
17pub struct PostSummaryData {
18    /// Whether the operation succeeded.
19    pub success: bool,
20    /// Number of files that were changed.
21    pub changed_files: usize,
22    /// Number of .pacnew files created.
23    pub pacnew_count: usize,
24    /// Number of .pacsave files created.
25    pub pacsave_count: usize,
26    /// List of service names that need to be restarted.
27    pub services_pending: Vec<String>,
28    /// Optional snapshot label if a snapshot was created.
29    pub snapshot_label: Option<String>,
30}
31
32/// What: Count changed files and collect affected systemd services for given packages.
33///
34/// Inputs:
35/// - `names`: Package names whose remote file lists should be inspected.
36///
37/// Output:
38/// - Returns a tuple with the number of file entries and a sorted list of service unit filenames.
39///
40/// Details:
41/// - Queries `pacman -Fl` per package, ignoring directory entries, and extracts `.service` paths.
42fn count_changed_files_and_services(names: &[String]) -> (usize, Vec<String>) {
43    let mut total_files: usize = 0;
44    let mut services: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
45    for name in names {
46        if let Ok(body) = crate::util::pacman::run_pacman(&["-Fl", name]) {
47            for line in body.lines() {
48                // pacman -Fl format: "<pkg> <path>"
49                if let Some((_pkg, path)) = line.split_once(' ') {
50                    if !path.ends_with('/') {
51                        total_files += 1;
52                    }
53                    if path.starts_with("/usr/lib/systemd/system/") && path.ends_with(".service") {
54                        // take filename
55                        if let Some(stem) = std::path::Path::new(path)
56                            .file_name()
57                            .and_then(|s| s.to_str())
58                        {
59                            services.insert(stem.to_string());
60                        }
61                    }
62                }
63            }
64        }
65    }
66    (total_files, services.into_iter().collect())
67}
68
69/// What: Scan `/etc` for outstanding `.pacnew` and `.pacsave` files.
70///
71/// Inputs:
72/// - (none): Walks the filesystem directly with a depth guard.
73///
74/// Output:
75/// - Returns counts of `.pacnew` and `.pacsave` files found beneath `/etc`.
76///
77/// Details:
78/// - Ignores very deep directory structures to avoid pathological traversal scenarios.
79fn count_pac_conflicts_in_etc() -> (usize, usize) {
80    fn walk(dir: &std::path::Path, pacnew: &mut usize, pacsave: &mut usize) {
81        if let Ok(rd) = std::fs::read_dir(dir) {
82            for entry in rd.flatten() {
83                let p = entry.path();
84                if p.is_dir() {
85                    // Limit to reasonable depth to avoid cycles (symlinks ignored)
86                    if p.strip_prefix("/etc")
87                        .is_ok_and(|stripped| stripped.components().count() > 12)
88                    {
89                        continue;
90                    }
91                    walk(&p, pacnew, pacsave);
92                } else if let Some(name) = p.file_name().and_then(|s| s.to_str()) {
93                    if name.ends_with(".pacnew") {
94                        *pacnew += 1;
95                    }
96                    if name.ends_with(".pacsave") {
97                        *pacsave += 1;
98                    }
99                }
100            }
101        }
102    }
103    let mut pn = 0usize;
104    let mut ps = 0usize;
105    walk(std::path::Path::new("/etc"), &mut pn, &mut ps);
106    (pn, ps)
107}
108
109/// What: Produce a best-effort summary of potential post-transaction tasks.
110///
111/// Inputs:
112/// - `items`: Packages that were part of the transaction and should inform the summary.
113/// - `success`: Execution result: `Some(true)` for success, `Some(false)` for failure, `None` if unknown.
114///
115/// Output:
116/// - Returns a `PostSummaryData` structure with file counts, service hints, and conflict tallies.
117///
118/// Details:
119/// - Combines sync database lookups with an `/etc` scan without performing system modifications.
120/// - Uses the provided `success` flag to indicate transaction outcome, defaulting to `false` if unknown.
121#[must_use]
122pub fn compute_post_summary(items: &[PackageItem], success: Option<bool>) -> PostSummaryData {
123    let names: Vec<String> = items.iter().map(|p| p.name.clone()).collect();
124    let (changed_files, services_pending) = count_changed_files_and_services(&names);
125    let (pacnew_count, pacsave_count) = count_pac_conflicts_in_etc();
126    PostSummaryData {
127        success: success.unwrap_or(false),
128        changed_files,
129        pacnew_count,
130        pacsave_count,
131        services_pending,
132        snapshot_label: None,
133    }
134}