1use super::{idx, save_to_disk};
2
3pub fn request_enrich_for(
18 persist_path: std::path::PathBuf,
19 notify_tx: tokio::sync::mpsc::UnboundedSender<()>,
20 names: Vec<String>,
21) {
22 tokio::spawn(async move {
23 use std::collections::HashSet;
25 const BATCH: usize = 100;
26 let set: HashSet<String> = names.into_iter().collect();
27 if set.is_empty() {
28 return;
29 }
30 let mut desc_map: std::collections::HashMap<String, (String, String, String, String)> =
32 std::collections::HashMap::new(); let all: Vec<String> = set.into_iter().collect();
34 for chunk in all.chunks(BATCH) {
35 let args_owned: Vec<String> = std::iter::once("-Si".to_string())
36 .chain(chunk.iter().cloned())
37 .collect();
38 let block = tokio::task::spawn_blocking(move || {
39 let args_ref: Vec<&str> = args_owned.iter().map(String::as_str).collect();
40 crate::util::pacman::run_pacman(&args_ref)
41 })
42 .await;
43 let Ok(Ok(out)) = block else { continue };
44 let mut cur_name: Option<String> = None;
46 let mut cur_desc: Option<String> = None;
47 let mut cur_arch: Option<String> = None;
48 let mut cur_repo: Option<String> = None;
49 let mut cur_ver: Option<String> = None;
50 #[allow(clippy::collection_is_never_read)]
51 let mut _cur_packager: Option<String> = None;
52 for line in out.lines().chain(std::iter::once("")) {
53 let line = line.trim_end();
54 if line.is_empty() {
55 if let Some(n) = cur_name.take() {
56 let d = cur_desc.take().unwrap_or_default();
57 let a = cur_arch.take().unwrap_or_default();
58 let r = cur_repo.take().unwrap_or_default();
59 let v = cur_ver.take().unwrap_or_default();
60
61 desc_map.insert(n, (d, a, r, v));
62 }
63 continue;
64 }
65 if let Some((k, v)) = line.split_once(':') {
66 let key = k.trim();
67 let val = v.trim();
68 match key {
69 "Name" => cur_name = Some(val.to_string()),
70 "Description" => cur_desc = Some(val.to_string()),
71 "Architecture" => cur_arch = Some(val.to_string()),
72 "Repository" => cur_repo = Some(val.to_string()),
73 "Packager" => _cur_packager = Some(val.to_string()),
74 "Version" => cur_ver = Some(val.to_string()),
75 _ => {}
76 }
77 }
78 }
79 }
80 if desc_map.is_empty() {
81 return;
82 }
83 if let Ok(mut g) = idx().write() {
85 for p in &mut g.pkgs {
86 if let Some((d, a, r, v)) = desc_map.get(&p.name) {
87 if p.description.is_empty() {
88 p.description = d.clone();
89 }
90 if !a.is_empty() {
91 p.arch = a.clone();
92 }
93 if !r.is_empty() {
94 p.repo = r.clone();
95 }
96 if !v.is_empty() {
97 p.version = v.clone();
98 }
99 }
100 }
101 }
102 save_to_disk(&persist_path);
103 let _ = notify_tx.send(());
104 });
105}
106
107#[cfg(test)]
108mod tests {
109 #[tokio::test]
110 async fn index_enrich_noop_on_empty_names() {
121 use std::path::PathBuf;
122 let mut path: PathBuf = std::env::temp_dir();
123 path.push(format!(
124 "pacsea_idx_empty_enrich_{}_{}.json",
125 std::process::id(),
126 std::time::SystemTime::now()
127 .duration_since(std::time::UNIX_EPOCH)
128 .expect("System time is before UNIX epoch")
129 .as_nanos()
130 ));
131 let idx_json = serde_json::json!({ "pkgs": [] });
132 std::fs::write(
133 &path,
134 serde_json::to_string(&idx_json).expect("Failed to serialize test index JSON"),
135 )
136 .expect("Failed to write test index file");
137 crate::index::load_from_disk(&path);
138
139 let (notify_tx, mut notify_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
140 super::request_enrich_for(path.clone(), notify_tx, Vec::new());
141 let none = tokio::time::timeout(std::time::Duration::from_millis(200), notify_rx.recv())
142 .await
143 .ok()
144 .flatten();
145 assert!(none.is_none());
146 let _ = std::fs::remove_file(&path);
147 }
148
149 #[cfg(not(target_os = "windows"))]
150 #[tokio::test]
151 #[allow(clippy::await_holding_lock)]
152 async fn enrich_updates_fields_and_notifies() {
163 let _guard = crate::global_test_mutex_lock();
164 if let Ok(mut g) = crate::index::idx().write() {
166 g.pkgs = vec![crate::index::OfficialPkg {
167 name: "foo".to_string(),
168 repo: String::new(),
169 arch: String::new(),
170 version: String::new(),
171 description: String::new(),
172 }];
173 }
174 let old_path = std::env::var("PATH").unwrap_or_default();
176 let mut root = std::env::temp_dir();
177 root.push(format!(
178 "pacsea_fake_pacman_si_{}_{}",
179 std::process::id(),
180 std::time::SystemTime::now()
181 .duration_since(std::time::UNIX_EPOCH)
182 .expect("System time is before UNIX epoch")
183 .as_nanos()
184 ));
185 std::fs::create_dir_all(&root).expect("Failed to create test root directory");
186 let mut bin = root.clone();
187 bin.push("bin");
188 std::fs::create_dir_all(&bin).expect("Failed to create test bin directory");
189 let mut script = bin.clone();
190 script.push("pacman");
191 let body = r#"#!/usr/bin/env bash
192set -e
193if [[ "$1" == "-Si" ]]; then
194 # Print two blocks, one for foo, one unrelated
195 cat <<EOF
196Name : foo
197Version : 1.2.3
198Architecture : x86_64
199Repository : core
200Description : hello
201
202Name : other
203Version : 9.9.9
204Architecture : any
205Repository : extra
206Description : nope
207EOF
208 exit 0
209fi
210exit 0
211"#;
212 std::fs::write(&script, body).expect("Failed to write test pacman script");
213 #[cfg(unix)]
214 {
215 use std::os::unix::fs::PermissionsExt;
216 let mut perm = std::fs::metadata(&script)
217 .expect("Failed to read test pacman script metadata")
218 .permissions();
219 perm.set_mode(0o755);
220 std::fs::set_permissions(&script, perm)
221 .expect("Failed to set test pacman script permissions");
222 }
223 let new_path = format!("{}:{old_path}", bin.to_string_lossy());
224 unsafe { std::env::set_var("PATH", &new_path) };
225
226 let mut path: std::path::PathBuf = std::env::temp_dir();
228 path.push("pacsea_enrich_test.json");
229 crate::index::save_to_disk(&path);
230
231 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<()>();
232 super::request_enrich_for(path.clone(), tx, vec!["foo".into(), "foo".into()]);
233 let notified = tokio::time::timeout(std::time::Duration::from_millis(500), rx.recv())
235 .await
236 .ok()
237 .flatten()
238 .is_some();
239 assert!(notified);
240
241 let all = crate::index::all_official();
243 let pkg = all
244 .iter()
245 .find(|p| p.name == "foo")
246 .expect("package 'foo' should exist in test data");
247 assert_eq!(pkg.version, "1.2.3");
248 assert_eq!(pkg.description, "hello");
249 match &pkg.source {
250 crate::state::Source::Official { repo, arch } => {
251 assert_eq!(repo, "core");
252 assert_eq!(arch, "x86_64");
253 }
254 crate::state::Source::Aur => panic!("expected official"),
255 }
256
257 unsafe { std::env::set_var("PATH", &old_path) };
259 let _ = std::fs::remove_file(&path);
260 let _ = std::fs::remove_dir_all(&root);
261 }
262}