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}