pacsea/logic/deps/
status.rs

1//! Dependency status determination and version checking.
2
3use crate::logic::deps::is_package_installed_or_provided;
4use crate::state::modal::DependencyStatus;
5use std::collections::HashSet;
6use std::process::{Command, Stdio};
7
8/// What: Evaluate a dependency's installation status relative to required versions.
9///
10/// Inputs:
11/// - `name`: Dependency package identifier.
12/// - `version_req`: Optional version constraint string (e.g., `>=1.2`).
13/// - `installed`: Set of names currently installed on the system.
14/// - `upgradable`: Set of names pacman reports as upgradable.
15///
16/// Output:
17/// - Returns a `DependencyStatus` describing whether installation, upgrade, or no action is needed.
18///
19/// Details:
20/// - Combines local database queries with helper functions to capture upgrade requirements and conflicts.
21pub(super) fn determine_status(
22    name: &str,
23    version_req: &str,
24    installed: &HashSet<String>,
25    provided: &HashSet<String>,
26    upgradable: &HashSet<String>,
27) -> DependencyStatus {
28    // Check if package is installed or provided by an installed package
29    if !is_package_installed_or_provided(name, installed, provided) {
30        return DependencyStatus::ToInstall;
31    }
32
33    // Check if package is upgradable (even without version requirement)
34    let is_upgradable = upgradable.contains(name);
35
36    // If version requirement is specified, check if it matches
37    if !version_req.is_empty() {
38        // Try to get installed version
39        if let Ok(installed_version) = get_installed_version(name) {
40            // Simple version comparison (basic implementation)
41            if !version_satisfies(&installed_version, version_req) {
42                return DependencyStatus::ToUpgrade {
43                    current: installed_version,
44                    required: version_req.to_string(),
45                };
46            }
47            // Version requirement satisfied, but check if package is upgradable anyway
48            if is_upgradable {
49                // Get available version from pacman -Si if possible
50                let available_version =
51                    get_available_version(name).unwrap_or_else(|| "newer".to_string());
52                return DependencyStatus::ToUpgrade {
53                    current: installed_version,
54                    required: available_version,
55                };
56            }
57            return DependencyStatus::Installed {
58                version: installed_version,
59            };
60        }
61    }
62
63    // Installed but no version check needed - check if upgradable
64    if is_upgradable {
65        match get_installed_version(name) {
66            Ok(current_version) => {
67                let available_version =
68                    get_available_version(name).unwrap_or_else(|| "newer".to_string());
69                return DependencyStatus::ToUpgrade {
70                    current: current_version,
71                    required: available_version,
72                };
73            }
74            Err(_) => {
75                return DependencyStatus::ToUpgrade {
76                    current: "installed".to_string(),
77                    required: "newer".to_string(),
78                };
79            }
80        }
81    }
82
83    // Installed and up-to-date - get actual version
84    get_installed_version(name).map_or_else(
85        |_| DependencyStatus::Installed {
86            version: "installed".to_string(),
87        },
88        |version| DependencyStatus::Installed { version },
89    )
90}
91
92/// What: Query the repositories for the latest available version of a package.
93///
94/// Inputs:
95/// - `name`: Package name looked up via `pacman -Si`.
96///
97/// Output:
98/// - Returns the version string advertised in the repositories, or `None` on failure.
99///
100/// Details:
101/// - Strips revision suffixes (e.g., `-1`) so comparisons focus on the base semantic version.
102pub(super) fn get_available_version(name: &str) -> Option<String> {
103    let output = Command::new("pacman")
104        .args(["-Si", name])
105        .env("LC_ALL", "C")
106        .env("LANG", "C")
107        .stdin(Stdio::null())
108        .stdout(Stdio::piped())
109        .stderr(Stdio::piped())
110        .output()
111        .ok()?;
112
113    if !output.status.success() {
114        return None;
115    }
116
117    let text = String::from_utf8_lossy(&output.stdout);
118    for line in text.lines() {
119        if line.starts_with("Version")
120            && let Some(colon_pos) = line.find(':')
121        {
122            let version = line[colon_pos + 1..].trim();
123            // Remove revision suffix if present
124            let version = version.split('-').next().unwrap_or(version);
125            return Some(version.to_string());
126        }
127    }
128    None
129}
130
131/// What: Retrieve the locally installed version of a package.
132///
133/// Inputs:
134/// - `name`: Package to query via `pacman -Q`.
135///
136/// Output:
137/// - Returns the installed version string on success; otherwise an error message.
138///
139/// # Errors
140/// - Returns `Err` when `pacman -Q` command execution fails (I/O error)
141/// - Returns `Err` when the package is not found or not installed
142/// - Returns `Err` when the version string cannot be parsed from command output
143///
144/// Details:
145/// - Normalizes versions by removing revision suffixes to facilitate requirement comparisons.
146pub fn get_installed_version(name: &str) -> Result<String, String> {
147    let output = Command::new("pacman")
148        .args(["-Q", name])
149        .env("LC_ALL", "C")
150        .env("LANG", "C")
151        .stdin(Stdio::null())
152        .stdout(Stdio::piped())
153        .stderr(Stdio::piped())
154        .output()
155        .map_err(|e| format!("pacman -Q failed: {e}"))?;
156
157    if !output.status.success() {
158        return Err("Package not found".to_string());
159    }
160
161    let text = String::from_utf8_lossy(&output.stdout);
162    if let Some(line) = text.lines().next() {
163        // Format: "name version" or "name version-revision"
164        if let Some(space_pos) = line.find(' ') {
165            let version = line[space_pos + 1..].trim();
166            // Remove revision suffix if present (e.g., "1.2.3-1" -> "1.2.3")
167            let version = version.split('-').next().unwrap_or(version);
168            return Ok(version.to_string());
169        }
170    }
171
172    Err("Could not parse version".to_string())
173}
174
175/// What: Perform a simplified comparison between an installed version and a requirement expression.
176///
177/// Inputs:
178/// - `installed`: Version string currently present on the system.
179/// - `requirement`: Comparison expression such as `>=1.2` or `=2.0`.
180///
181/// Output:
182/// - `true` when the expression evaluates in favor of the installed version; otherwise `false`.
183///
184/// Details:
185/// - Uses straightforward string comparisons rather than full semantic version parsing, matching pacman's format.
186#[must_use]
187pub fn version_satisfies(installed: &str, requirement: &str) -> bool {
188    // This is a simplified version checker
189    // For production, use a proper version comparison library
190    requirement.strip_prefix(">=").map_or_else(
191        || {
192            requirement.strip_prefix("<=").map_or_else(
193                || {
194                    requirement.strip_prefix("=").map_or_else(
195                        || {
196                            requirement.strip_prefix(">").map_or_else(
197                                || {
198                                    requirement.strip_prefix("<").map_or_else(
199                                        || {
200                                            // No version requirement, assume satisfied
201                                            true
202                                        },
203                                        |req_ver| installed < req_ver,
204                                    )
205                                },
206                                |req_ver| installed > req_ver,
207                            )
208                        },
209                        |req_ver| installed == req_ver,
210                    )
211                },
212                |req_ver| installed <= req_ver,
213            )
214        },
215        |req_ver| installed >= req_ver,
216    )
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    /// What: Ensure relational comparison operators behave according to the simplified string checks.
225    ///
226    /// Inputs:
227    /// - `>=`, `<=`, `>`, `<`, and `=` requirements evaluated against representative version strings.
228    ///
229    /// Output:
230    /// - Verifies truthiness for matching cases and falseness for mismatched comparisons.
231    ///
232    /// Details:
233    /// - Confirms the helper remains stable for the ordering relied upon by dependency diagnostics.
234    fn version_satisfies_relational_operators() {
235        assert!(version_satisfies("2.0", ">=1.5"));
236        assert!(!version_satisfies("1.0", ">=1.5"));
237        assert!(version_satisfies("1.5", "<=1.5"));
238        assert!(version_satisfies("1.6", ">1.5"));
239        assert!(!version_satisfies("1.4", ">1.5"));
240        assert!(version_satisfies("1.5", "=1.5"));
241        assert!(!version_satisfies("1.6", "<1.5"));
242    }
243
244    #[test]
245    /// What: Confirm the helper defaults to success when no requirement string is provided.
246    ///
247    /// Inputs:
248    /// - Empty and non-operator requirement strings.
249    ///
250    /// Output:
251    /// - Returns `true`, indicating no additional comparison is enforced.
252    ///
253    /// Details:
254    /// - Guards the fallback branch used by callers that lack explicit version constraints.
255    fn version_satisfies_defaults_to_true_without_constraint() {
256        assert!(version_satisfies("2.0", ""));
257        assert!(version_satisfies("2.0", "n/a"));
258    }
259}