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}