Skip to main content

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}