pacsea/logic/files/
lists.rs

1//! File list retrieval functions for remote and installed packages.
2
3use super::pkgbuild_cache::{PkgbuildSourceKind, parse_pkgbuild_cached};
4use super::pkgbuild_fetch::fetch_pkgbuild_sync;
5use crate::state::types::Source;
6use std::process::Command;
7
8/// What: Parse file list from pacman/paru/yay command output.
9///
10/// Inputs:
11/// - `output`: Command output containing file list in format "<pkg> <path>".
12///
13/// Output:
14/// - Returns vector of file paths extracted from the output.
15///
16/// Details:
17/// - Parses lines in format "<pkg> <path>" and extracts the path component.
18fn parse_file_list_from_output(output: &[u8]) -> Vec<String> {
19    let text = String::from_utf8_lossy(output);
20    text.lines()
21        .filter_map(|line| line.split_once(' ').map(|(_pkg, path)| path.to_string()))
22        .collect()
23}
24
25/// What: Try to get file list using an AUR helper command (paru or yay).
26///
27/// Inputs:
28/// - `helper`: Name of the helper command ("paru" or "yay").
29/// - `name`: Package name to query.
30///
31/// Output:
32/// - Returns Some(Vec<String>) if successful, None otherwise.
33///
34/// Details:
35/// - Executes helper -Fl command and parses the output.
36/// - Returns None if command fails or produces no files.
37fn try_aur_helper_file_list(helper: &str, name: &str) -> Option<Vec<String>> {
38    tracing::debug!("Trying {} -Fl {} for AUR package file list", helper, name);
39    let output = Command::new(helper)
40        .args(["-Fl", name])
41        .env("LC_ALL", "C")
42        .env("LANG", "C")
43        .output()
44        .ok()?;
45
46    if !output.status.success() {
47        return None;
48    }
49
50    let files = parse_file_list_from_output(&output.stdout);
51    if files.is_empty() {
52        return None;
53    }
54
55    tracing::debug!(
56        "Found {} files from {} -Fl for {}",
57        files.len(),
58        helper,
59        name
60    );
61    Some(files)
62}
63
64/// What: Get file list for AUR package using multiple fallback strategies.
65///
66/// Inputs:
67/// - `name`: Package name to query.
68///
69/// Output:
70/// - Returns file list if found, empty vector if no sources available.
71///
72/// Details:
73/// - Tries installed files, then paru/yay, then PKGBUILD parsing.
74fn get_aur_file_list(name: &str) -> Vec<String> {
75    // First, check if package is already installed
76    if let Ok(installed_files) = get_installed_file_list(name)
77        && !installed_files.is_empty()
78    {
79        tracing::debug!(
80            "Found {} files from installed AUR package {}",
81            installed_files.len(),
82            name
83        );
84        return installed_files;
85    }
86
87    // Try to use paru/yay -Fl if available (works for cached AUR packages)
88    let has_paru = Command::new("paru").args(["--version"]).output().is_ok();
89    let has_yay = Command::new("yay").args(["--version"]).output().is_ok();
90
91    if has_paru && let Some(files) = try_aur_helper_file_list("paru", name) {
92        return files;
93    }
94
95    if has_yay && let Some(files) = try_aur_helper_file_list("yay", name) {
96        return files;
97    }
98
99    // Fallback: try to parse PKGBUILD to extract install paths
100    if let Ok(pkgbuild) = fetch_pkgbuild_sync(name) {
101        let entry = parse_pkgbuild_cached(name, None, PkgbuildSourceKind::Aur, &pkgbuild);
102        let files = entry.install_paths;
103        if !files.is_empty() {
104            tracing::debug!(
105                "Found {} files from PKGBUILD parsing for {}",
106                files.len(),
107                name
108            );
109            return files;
110        }
111    } else {
112        tracing::debug!("Failed to fetch PKGBUILD for {}", name);
113    }
114
115    // No file list available
116    tracing::debug!(
117        "AUR package {}: file list not available (not installed, not cached, PKGBUILD parsing failed)",
118        name
119    );
120    Vec::new()
121}
122
123/// What: Get file list for official repository package.
124///
125/// Inputs:
126/// - `name`: Package name to query.
127/// - `repo`: Repository name (empty string if not specified).
128///
129/// Output:
130/// - Returns file list or error if command fails.
131///
132/// Details:
133/// - Uses pacman -Fl command. Returns empty list if file database is not synced.
134fn get_official_file_list(name: &str, repo: &str) -> Result<Vec<String>, String> {
135    tracing::debug!("Running: pacman -Fl {}", name);
136    let spec = if repo.is_empty() {
137        name.to_string()
138    } else {
139        format!("{repo}/{name}")
140    };
141
142    let output = Command::new("pacman")
143        .args(["-Fl", &spec])
144        .env("LC_ALL", "C")
145        .env("LANG", "C")
146        .output()
147        .map_err(|e| {
148            tracing::error!("Failed to execute pacman -Fl {}: {}", spec, e);
149            format!("pacman -Fl failed: {e}")
150        })?;
151
152    if !output.status.success() {
153        let stderr = String::from_utf8_lossy(&output.stderr);
154        // Check if error is due to missing file database
155        if stderr.contains("database file") && stderr.contains("does not exist") {
156            tracing::warn!(
157                "File database not synced for {} (pacman -Fy requires root). Skipping file list.",
158                name
159            );
160            return Ok(Vec::new()); // Return empty instead of error
161        }
162        tracing::error!(
163            "pacman -Fl {} failed with status {:?}: {}",
164            spec,
165            output.status.code(),
166            stderr
167        );
168        return Err(format!("pacman -Fl failed for {spec}: {stderr}"));
169    }
170
171    let files = parse_file_list_from_output(&output.stdout);
172    tracing::debug!("Found {} files in remote package {}", files.len(), name);
173    Ok(files)
174}
175
176/// What: Fetch the list of files published in repositories for a given package.
177///
178/// Inputs:
179/// - `name`: Package name in question.
180/// - `source`: Source descriptor differentiating official repositories from AUR packages.
181///
182/// Output:
183/// - Returns the list of file paths or an error when retrieval fails.
184///
185/// # Errors
186/// - Returns `Err` when `pacman -Fl` command execution fails for official packages
187/// - Returns `Err` when file database is not synced and command fails
188///
189/// Details:
190/// - Uses `pacman -Fl` for official packages and currently returns an empty list for AUR entries.
191pub fn get_remote_file_list(name: &str, source: &Source) -> Result<Vec<String>, String> {
192    match source {
193        Source::Official { repo, .. } => get_official_file_list(name, repo),
194        Source::Aur => Ok(get_aur_file_list(name)),
195    }
196}
197
198/// What: Retrieve the list of files currently installed for a package.
199///
200/// Inputs:
201/// - `name`: Package name queried via `pacman -Ql`.
202///
203/// Output:
204/// - Returns file paths owned by the package or an empty list when it is not installed.
205///
206/// # Errors
207/// - Returns `Err` when `pacman -Ql` command execution fails (I/O error)
208/// - Returns `Err` when `pacman -Ql` exits with non-zero status for reasons other than package not found
209///
210/// Details:
211/// - Logs errors if the command fails for reasons other than the package being absent.
212pub fn get_installed_file_list(name: &str) -> Result<Vec<String>, String> {
213    tracing::debug!("Running: pacman -Ql {}", name);
214    let output = Command::new("pacman")
215        .args(["-Ql", name])
216        .env("LC_ALL", "C")
217        .env("LANG", "C")
218        .output()
219        .map_err(|e| {
220            tracing::error!("Failed to execute pacman -Ql {}: {}", name, e);
221            format!("pacman -Ql failed: {e}")
222        })?;
223
224    if !output.status.success() {
225        // Package not installed - this is OK for install operations
226        let stderr = String::from_utf8_lossy(&output.stderr);
227        if stderr.contains("was not found") {
228            tracing::debug!("Package {} is not installed", name);
229            return Ok(Vec::new());
230        }
231        tracing::error!(
232            "pacman -Ql {} failed with status {:?}: {}",
233            name,
234            output.status.code(),
235            stderr
236        );
237        return Err(format!("pacman -Ql failed for {name}: {stderr}"));
238    }
239
240    let files = parse_file_list_from_output(&output.stdout);
241    tracing::debug!("Found {} files in installed package {}", files.len(), name);
242    Ok(files)
243}