arch_toolkit/index/installed.rs
1//! Installed package query functions for the index module.
2
3use std::collections::HashSet;
4use std::process::{Command, Stdio};
5
6use crate::error::{ArchToolkitError, Result};
7
8/// What: Query pacman for all installed packages and optionally update a cache.
9///
10/// Inputs:
11/// - `cache`: Optional mutable reference to a `HashSet<String>` to update with results.
12///
13/// Output:
14/// - Returns `Ok(HashSet<String>)` containing all installed package names.
15/// - Returns `Ok(HashSet::new())` on failure (graceful degradation).
16///
17/// Details:
18/// - Uses `pacman -Qq` to query the local database.
19/// - If `cache` is provided, updates it with the results.
20/// - Sets `LC_ALL=C` and `LANG=C` for consistent locale-independent output.
21/// - Logs errors for diagnostics but returns empty set to avoid blocking operations.
22///
23/// # Errors
24///
25/// This function does not return errors - it gracefully degrades by returning an empty set.
26/// Errors are logged using `tracing::error` for diagnostics.
27///
28/// # Example
29///
30/// ```no_run
31/// use arch_toolkit::index::refresh_installed_cache;
32/// use std::collections::HashSet;
33///
34/// let mut cache = HashSet::new();
35/// let packages = refresh_installed_cache(Some(&mut cache)).unwrap();
36/// println!("Found {} installed packages", packages.len());
37/// ```
38#[allow(clippy::implicit_hasher)]
39pub fn refresh_installed_cache(cache: Option<&mut HashSet<String>>) -> Result<HashSet<String>> {
40 tracing::debug!("Running: pacman -Qq");
41 let output = Command::new("pacman")
42 .args(["-Qq"])
43 .env("LC_ALL", "C")
44 .env("LANG", "C")
45 .stdin(Stdio::null())
46 .stdout(Stdio::piped())
47 .stderr(Stdio::piped())
48 .output();
49
50 let packages = match output {
51 Ok(output) => {
52 if output.status.success() {
53 let text = String::from_utf8_lossy(&output.stdout);
54 let packages: HashSet<String> = text
55 .lines()
56 .map(|s| s.trim().to_string())
57 .filter(|s| !s.is_empty())
58 .collect();
59 tracing::debug!(
60 "Successfully retrieved {} installed packages",
61 packages.len()
62 );
63 packages
64 } else {
65 let stderr = String::from_utf8_lossy(&output.stderr);
66 tracing::error!(
67 "pacman -Qq failed with status {:?}: {}",
68 output.status.code(),
69 stderr
70 );
71 HashSet::new()
72 }
73 }
74 Err(e) => {
75 tracing::error!("Failed to execute pacman -Qq: {}", e);
76 HashSet::new()
77 }
78 };
79
80 // Update cache if provided
81 if let Some(cache_ref) = cache {
82 cache_ref.clone_from(&packages);
83 }
84
85 Ok(packages)
86}
87
88/// What: Query pacman for all installed packages asynchronously and optionally update a cache.
89///
90/// Inputs:
91/// - `cache`: Optional mutable reference to a `HashSet<String>` to update with results.
92///
93/// Output:
94/// - Returns a future that resolves to `Result<HashSet<String>>` containing all installed package names.
95///
96/// Details:
97/// - Uses `tokio::task::spawn_blocking` to run the sync version in a blocking task.
98/// - If `cache` is provided, updates it with the results after the task completes.
99/// - The cache parameter must be `Send` and `Sync` to be used across async boundaries.
100///
101/// # Errors
102///
103/// Returns `Err` if the blocking task fails, otherwise returns the same result as the sync version.
104///
105/// # Example
106///
107/// ```no_run
108/// use arch_toolkit::index::refresh_installed_cache_async;
109/// use std::collections::HashSet;
110///
111/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
112/// let mut cache = HashSet::new();
113/// let packages = refresh_installed_cache_async(Some(&mut cache)).await?;
114/// println!("Found {} installed packages", packages.len());
115/// # Ok(())
116/// # }
117/// ```
118#[cfg(feature = "index")]
119#[allow(clippy::implicit_hasher)]
120pub async fn refresh_installed_cache_async(
121 cache: Option<&mut HashSet<String>>,
122) -> Result<HashSet<String>> {
123 // Run the blocking operation without the cache parameter
124 let result = tokio::task::spawn_blocking(|| refresh_installed_cache(None))
125 .await
126 .map_err(|e| ArchToolkitError::Parse(format!("Blocking task failed: {e}")))?;
127
128 // Update cache if provided and result is successful
129 if let (Ok(packages), Some(cache_ref)) = (result.as_ref(), cache) {
130 cache_ref.clone_from(packages);
131 }
132
133 result
134}
135
136/// What: Check if a package is installed, using cache if provided or querying pacman directly.
137///
138/// Inputs:
139/// - `name`: Package name to check.
140/// - `cache`: Optional reference to a `HashSet<String>` containing installed package names.
141///
142/// Output:
143/// - Returns `true` if the package is installed, `false` otherwise.
144///
145/// Details:
146/// - If `cache` is provided, checks membership in the cache (O(1) lookup).
147/// - If `cache` is `None`, queries pacman directly using `pacman -Q`.
148/// - Gracefully degrades: returns `false` on error.
149///
150/// # Example
151///
152/// ```no_run
153/// use arch_toolkit::index::is_installed;
154/// use std::collections::HashSet;
155///
156/// let cache = HashSet::from(["vim".to_string(), "git".to_string()]);
157/// assert!(is_installed("vim", Some(&cache)));
158/// assert!(!is_installed("nonexistent", Some(&cache)));
159/// ```
160#[must_use]
161#[allow(clippy::implicit_hasher)]
162pub fn is_installed(name: &str, cache: Option<&HashSet<String>>) -> bool {
163 if let Some(cache_ref) = cache {
164 return cache_ref.contains(name);
165 }
166
167 // Query pacman directly if no cache
168 tracing::debug!("Running: pacman -Q {}", name);
169 let output = Command::new("pacman")
170 .args(["-Q", name])
171 .env("LC_ALL", "C")
172 .env("LANG", "C")
173 .stdin(Stdio::null())
174 .stdout(Stdio::piped())
175 .stderr(Stdio::piped())
176 .output();
177
178 match output {
179 Ok(output) => output.status.success(),
180 Err(e) => {
181 tracing::error!("Failed to execute pacman -Q {}: {}", name, e);
182 false
183 }
184 }
185}
186
187/// What: Query pacman directly for all installed packages without caching.
188///
189/// Inputs:
190/// - None: Invokes `pacman -Qq` to query the local database.
191///
192/// Output:
193/// - Returns `Ok(HashSet<String>)` containing all installed package names.
194/// - Returns `Ok(HashSet::new())` on failure (graceful degradation).
195///
196/// Details:
197/// - Direct query to pacman, no caching involved.
198/// - Reuses the same logic as `refresh_installed_cache` but without cache parameter.
199/// - Sets `LC_ALL=C` and `LANG=C` for consistent locale-independent output.
200///
201/// # Errors
202///
203/// This function does not return errors - it gracefully degrades by returning an empty set.
204/// Errors are logged using `tracing::error` for diagnostics.
205///
206/// # Example
207///
208/// ```no_run
209/// use arch_toolkit::index::get_installed_packages;
210///
211/// let packages = get_installed_packages().unwrap();
212/// println!("Found {} installed packages", packages.len());
213/// ```
214pub fn get_installed_packages() -> Result<HashSet<String>> {
215 refresh_installed_cache(None)
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 /// What: Verify `refresh_installed_cache` updates cache when provided.
224 ///
225 /// Inputs:
226 /// - Empty cache and function call with cache parameter.
227 ///
228 /// Output:
229 /// - Cache is populated with results (if pacman is available).
230 ///
231 /// Details:
232 /// - Tests that cache parameter is updated correctly.
233 fn refresh_installed_cache_updates_cache() {
234 let mut cache = HashSet::new();
235 let _result = refresh_installed_cache(Some(&mut cache));
236 // Cache should be updated (may be empty if pacman unavailable, which is OK)
237 // We can't assert specific contents without knowing system state
238 }
239
240 #[test]
241 /// What: Verify `refresh_installed_cache` works without cache parameter.
242 ///
243 /// Inputs:
244 /// - Function call without cache parameter.
245 ///
246 /// Output:
247 /// - Returns `HashSet` (may be empty if pacman unavailable).
248 ///
249 /// Details:
250 /// - Tests that function works correctly when no cache is provided.
251 fn refresh_installed_cache_without_cache() {
252 let result = refresh_installed_cache(None);
253 assert!(result.is_ok());
254 // Result may be empty if pacman unavailable, which is graceful degradation
255 }
256
257 #[test]
258 /// What: Verify `is_installed` uses cache when provided.
259 ///
260 /// Inputs:
261 /// - Package name and cache containing the package.
262 ///
263 /// Output:
264 /// - Returns `true` for cached package, `false` for non-cached package.
265 ///
266 /// Details:
267 /// - Tests that cache lookup works correctly.
268 fn is_installed_uses_cache() {
269 let cache = HashSet::from(["vim".to_string(), "git".to_string()]);
270 assert!(is_installed("vim", Some(&cache)));
271 assert!(is_installed("git", Some(&cache)));
272 assert!(!is_installed("nonexistent", Some(&cache)));
273 }
274
275 #[test]
276 /// What: Verify `is_installed` queries pacman when cache is not provided.
277 ///
278 /// Inputs:
279 /// - Package name without cache parameter.
280 ///
281 /// Output:
282 /// - Returns result from pacman query (may be false if pacman unavailable).
283 ///
284 /// Details:
285 /// - Tests that function falls back to direct pacman query.
286 fn is_installed_without_cache() {
287 // This will query pacman directly
288 // Result depends on system state, but should not panic
289 let _result = is_installed("vim", None);
290 }
291
292 #[test]
293 /// What: Verify `get_installed_packages` returns `HashSet`.
294 ///
295 /// Inputs:
296 /// - None: Direct query to pacman.
297 ///
298 /// Output:
299 /// - Returns `Ok(HashSet<String>)` (may be empty if pacman unavailable).
300 ///
301 /// Details:
302 /// - Tests that function returns correct type and handles errors gracefully.
303 fn get_installed_packages_returns_hashset() {
304 let result = get_installed_packages();
305 assert!(result.is_ok());
306 // Result may be empty if pacman unavailable, which is graceful degradation
307 }
308
309 #[cfg(feature = "index")]
310 #[tokio::test]
311 /// What: Verify `refresh_installed_cache_async` works asynchronously.
312 ///
313 /// Inputs:
314 /// - Async function call with optional cache.
315 ///
316 /// Output:
317 /// - Returns future that resolves to `HashSet`.
318 ///
319 /// Details:
320 /// - Tests that async version works correctly.
321 async fn refresh_installed_cache_async_works() {
322 let mut cache = HashSet::new();
323 let result = refresh_installed_cache_async(Some(&mut cache)).await;
324 assert!(result.is_ok());
325 // Result may be empty if pacman unavailable, which is graceful degradation
326 }
327}