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