pacsea/logic/files/
backup.rs

1//! Backup file detection and retrieval functions.
2
3use super::pkgbuild_cache::{PkgbuildSourceKind, parse_pkgbuild_cached};
4use super::pkgbuild_fetch::{fetch_pkgbuild_sync, fetch_srcinfo_sync};
5use super::pkgbuild_parse::parse_backup_from_srcinfo;
6use crate::state::types::Source;
7use std::process::Command;
8
9/// What: Identify files marked for backup handling during install or removal operations.
10///
11/// Inputs:
12/// - `name`: Package whose backup array should be inspected.
13/// - `source`: Source descriptor to decide how to gather backup information.
14///
15/// Output:
16/// - Returns a list of backup file paths or an empty list when the data cannot be retrieved.
17///
18/// # Errors
19/// - Returns `Err` when `pacman -Qii` command execution fails for installed packages
20/// - Returns `Err` when PKGBUILD or .SRCINFO fetch fails and no fallback is available
21///
22/// Details:
23/// - Prefers querying the installed package via `pacman -Qii`; falls back to best-effort heuristics.
24pub fn get_backup_files(name: &str, source: &Source) -> Result<Vec<String>, String> {
25    // First try: if package is installed, use pacman -Qii
26    if let Ok(backup_files) = get_backup_files_from_installed(name)
27        && !backup_files.is_empty()
28    {
29        tracing::debug!(
30            "Found {} backup files from installed package {}",
31            backup_files.len(),
32            name
33        );
34        return Ok(backup_files);
35    }
36
37    // Second try: parse from PKGBUILD/.SRCINFO (best-effort, may fail)
38    match source {
39        Source::Official { .. } => {
40            // Try to fetch PKGBUILD and parse backup array
41            match fetch_pkgbuild_sync(name) {
42                Ok(pkgbuild) => {
43                    let entry =
44                        parse_pkgbuild_cached(name, None, PkgbuildSourceKind::Official, &pkgbuild);
45                    let backup_files = entry.backup_files;
46                    if !backup_files.is_empty() {
47                        tracing::debug!(
48                            "Found {} backup files from PKGBUILD for {}",
49                            backup_files.len(),
50                            name
51                        );
52                        return Ok(backup_files);
53                    }
54                }
55                Err(e) => {
56                    tracing::debug!("Failed to fetch PKGBUILD for {}: {}", name, e);
57                }
58            }
59            Ok(Vec::new())
60        }
61        Source::Aur => {
62            // Try to fetch .SRCINFO first (more reliable for AUR)
63            match fetch_srcinfo_sync(name) {
64                Ok(srcinfo) => {
65                    let backup_files = parse_backup_from_srcinfo(&srcinfo);
66                    if !backup_files.is_empty() {
67                        tracing::debug!(
68                            "Found {} backup files from .SRCINFO for {}",
69                            backup_files.len(),
70                            name
71                        );
72                        return Ok(backup_files);
73                    }
74                }
75                Err(e) => {
76                    tracing::debug!("Failed to fetch .SRCINFO for {}: {}", name, e);
77                }
78            }
79            // Fallback to PKGBUILD if .SRCINFO failed
80            match fetch_pkgbuild_sync(name) {
81                Ok(pkgbuild) => {
82                    let entry =
83                        parse_pkgbuild_cached(name, None, PkgbuildSourceKind::Aur, &pkgbuild);
84                    let backup_files = entry.backup_files;
85                    if !backup_files.is_empty() {
86                        tracing::debug!(
87                            "Found {} backup files from PKGBUILD for {}",
88                            backup_files.len(),
89                            name
90                        );
91                        return Ok(backup_files);
92                    }
93                }
94                Err(e) => {
95                    tracing::debug!("Failed to fetch PKGBUILD for {}: {}", name, e);
96                }
97            }
98            Ok(Vec::new())
99        }
100    }
101}
102
103/// What: Collect backup file entries for an installed package through `pacman -Qii`.
104///
105/// Inputs:
106/// - `name`: Installed package identifier.
107///
108/// Output:
109/// - Returns the backup array as a vector of file paths or an empty list when not installed.
110///
111/// # Errors
112/// - Returns `Err` when `pacman -Qii` command execution fails (I/O error)
113/// - Returns `Err` when `pacman -Qii` exits with non-zero status for reasons other than package not found
114///
115/// Details:
116/// - Parses the `Backup Files` section, handling wrapped lines to ensure complete coverage.
117pub fn get_backup_files_from_installed(name: &str) -> Result<Vec<String>, String> {
118    tracing::debug!("Running: pacman -Qii {}", name);
119    let output = Command::new("pacman")
120        .args(["-Qii", name])
121        .env("LC_ALL", "C")
122        .env("LANG", "C")
123        .output()
124        .map_err(|e| {
125            tracing::error!("Failed to execute pacman -Qii {}: {}", name, e);
126            format!("pacman -Qii failed: {e}")
127        })?;
128
129    if !output.status.success() {
130        // Package not installed - this is OK
131        let stderr = String::from_utf8_lossy(&output.stderr);
132        if stderr.contains("was not found") {
133            tracing::debug!("Package {} is not installed", name);
134            return Ok(Vec::new());
135        }
136        tracing::error!(
137            "pacman -Qii {} failed with status {:?}: {}",
138            name,
139            output.status.code(),
140            stderr
141        );
142        return Err(format!("pacman -Qii failed for {name}: {stderr}"));
143    }
144
145    let text = String::from_utf8_lossy(&output.stdout);
146    let mut backup_files = Vec::new();
147    let mut in_backup_section = false;
148
149    // Parse pacman -Qii output: look for "Backup Files" field
150    for line in text.lines() {
151        if line.starts_with("Backup Files") {
152            in_backup_section = true;
153            // Extract files from the same line if present
154            if let Some(colon_pos) = line.find(':') {
155                let files_str = line[colon_pos + 1..].trim();
156                if !files_str.is_empty() && files_str != "None" {
157                    for file in files_str.split_whitespace() {
158                        backup_files.push(file.to_string());
159                    }
160                }
161            }
162        } else if in_backup_section {
163            // Continuation lines (indented)
164            if line.starts_with("    ") || line.starts_with('\t') {
165                for file in line.split_whitespace() {
166                    backup_files.push(file.to_string());
167                }
168            } else {
169                // End of backup section
170                break;
171            }
172        }
173    }
174
175    tracing::debug!(
176        "Found {} backup files for installed package {}",
177        backup_files.len(),
178        name
179    );
180    Ok(backup_files)
181}