1use super::{OfficialPkg, idx, save_to_disk};
2
3const fn official_entry_needs_si_fill(p: &OfficialPkg) -> bool {
16 p.description.is_empty() || p.arch.is_empty()
17}
18
19pub fn request_enrich_for(
37 persist_path: std::path::PathBuf,
38 notify_tx: tokio::sync::mpsc::UnboundedSender<()>,
39 names: Vec<String>,
40) {
41 tokio::spawn(async move {
42 use std::collections::HashSet;
44 const BATCH: usize = 100;
45 let set: HashSet<String> = names.into_iter().collect();
46 if set.is_empty() {
47 return;
48 }
49 let names_to_fetch: Vec<String> = {
50 let Ok(guard) = idx().read() else {
51 return;
52 };
53 set.into_iter()
54 .filter(|n| {
55 guard
56 .name_to_idx
57 .get(&n.to_lowercase())
58 .is_some_and(|i| official_entry_needs_si_fill(&guard.pkgs[*i]))
59 })
60 .collect()
61 };
62 if names_to_fetch.is_empty() {
63 return;
64 }
65 let mut desc_map: std::collections::HashMap<String, (String, String, String, String)> =
67 std::collections::HashMap::new(); for chunk in names_to_fetch.chunks(BATCH) {
69 let args_owned: Vec<String> = std::iter::once("-Si".to_string())
70 .chain(chunk.iter().cloned())
71 .collect();
72 let block = tokio::task::spawn_blocking(move || {
73 let args_ref: Vec<&str> = args_owned.iter().map(String::as_str).collect();
74 crate::util::pacman::run_pacman(&args_ref)
75 })
76 .await;
77 let Ok(Ok(out)) = block else { continue };
78 let mut cur_name: Option<String> = None;
80 let mut cur_desc: Option<String> = None;
81 let mut cur_arch: Option<String> = None;
82 let mut cur_repo: Option<String> = None;
83 let mut cur_ver: Option<String> = None;
84 #[allow(clippy::collection_is_never_read)]
85 let mut _cur_packager: Option<String> = None;
86 for line in out.lines().chain(std::iter::once("")) {
87 let line = line.trim_end();
88 if line.is_empty() {
89 if let Some(n) = cur_name.take() {
90 let d = cur_desc.take().unwrap_or_default();
91 let a = cur_arch.take().unwrap_or_default();
92 let r = cur_repo.take().unwrap_or_default();
93 let v = cur_ver.take().unwrap_or_default();
94
95 desc_map.insert(n, (d, a, r, v));
96 }
97 continue;
98 }
99 if let Some((k, v)) = line.split_once(':') {
100 let key = k.trim();
101 let val = v.trim();
102 match key {
103 "Name" => cur_name = Some(val.to_string()),
104 "Description" => cur_desc = Some(val.to_string()),
105 "Architecture" => cur_arch = Some(val.to_string()),
106 "Repository" => cur_repo = Some(val.to_string()),
107 "Packager" => _cur_packager = Some(val.to_string()),
108 "Version" => cur_ver = Some(val.to_string()),
109 _ => {}
110 }
111 }
112 }
113 }
114 if desc_map.is_empty() {
115 return;
116 }
117 let mut index_dirty = false;
119 if let Ok(mut g) = idx().write() {
120 for p in &mut g.pkgs {
121 if let Some((d, a, r, v)) = desc_map.get(&p.name) {
122 if p.description.is_empty() && !d.is_empty() {
123 p.description.clone_from(d);
124 index_dirty = true;
125 }
126 if !a.is_empty() && p.arch != *a {
127 p.arch.clone_from(a);
128 index_dirty = true;
129 }
130 if !r.is_empty() && p.repo != *r {
131 p.repo.clone_from(r);
132 index_dirty = true;
133 }
134 if !v.is_empty() && p.version != *v {
135 p.version.clone_from(v);
136 index_dirty = true;
137 }
138 }
139 }
140 }
141 if index_dirty {
142 save_to_disk(&persist_path);
143 let _ = notify_tx.send(());
144 }
145 });
146}
147
148#[cfg(test)]
149mod tests {
150 #[tokio::test]
151 async fn index_enrich_noop_on_empty_names() {
162 use std::path::PathBuf;
163 let mut path: PathBuf = std::env::temp_dir();
164 path.push(format!(
165 "pacsea_idx_empty_enrich_{}_{}.json",
166 std::process::id(),
167 std::time::SystemTime::now()
168 .duration_since(std::time::UNIX_EPOCH)
169 .expect("System time is before UNIX epoch")
170 .as_nanos()
171 ));
172 let idx_json = serde_json::json!({ "pkgs": [] });
173 std::fs::write(
174 &path,
175 serde_json::to_string(&idx_json).expect("Failed to serialize test index JSON"),
176 )
177 .expect("Failed to write test index file");
178 crate::index::load_from_disk(&path);
179
180 let (notify_tx, mut notify_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
181 super::request_enrich_for(path.clone(), notify_tx, Vec::new());
182 let none = tokio::time::timeout(std::time::Duration::from_millis(200), notify_rx.recv())
183 .await
184 .ok()
185 .flatten();
186 assert!(none.is_none());
187 let _ = std::fs::remove_file(&path);
188 }
189
190 #[tokio::test]
191 #[allow(clippy::await_holding_lock)]
192 async fn enrich_skips_when_rows_already_filled() {
203 let _guard = crate::global_test_mutex_lock();
204 if let Ok(mut g) = crate::index::idx().write() {
205 g.pkgs = vec![crate::index::OfficialPkg {
206 name: "foo".to_string(),
207 repo: "core".to_string(),
208 arch: "x86_64".to_string(),
209 version: "1.0.0".to_string(),
210 description: "already filled".to_string(),
211 }];
212 g.rebuild_name_index();
213 }
214 let mut path = std::env::temp_dir();
215 path.push(format!(
216 "pacsea_enrich_skip_{}_{}.json",
217 std::process::id(),
218 std::time::SystemTime::now()
219 .duration_since(std::time::UNIX_EPOCH)
220 .expect("System time is before UNIX epoch")
221 .as_nanos()
222 ));
223 let (notify_tx, mut notify_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
224 super::request_enrich_for(path.clone(), notify_tx, vec!["foo".into()]);
225 let none = tokio::time::timeout(std::time::Duration::from_millis(400), notify_rx.recv())
226 .await
227 .ok()
228 .flatten();
229 assert!(none.is_none());
230 let _ = std::fs::remove_file(&path);
231 }
232
233 #[cfg(not(target_os = "windows"))]
234 #[tokio::test]
235 #[allow(clippy::await_holding_lock)]
236 async fn enrich_updates_fields_and_notifies() {
247 let _guard = crate::global_test_mutex_lock();
248 if let Ok(mut g) = crate::index::idx().write() {
250 g.pkgs = vec![crate::index::OfficialPkg {
251 name: "foo".to_string(),
252 repo: String::new(),
253 arch: String::new(),
254 version: String::new(),
255 description: String::new(),
256 }];
257 g.rebuild_name_index();
258 }
259 let old_path = std::env::var("PATH").unwrap_or_default();
261 let mut root = std::env::temp_dir();
262 root.push(format!(
263 "pacsea_fake_pacman_si_{}_{}",
264 std::process::id(),
265 std::time::SystemTime::now()
266 .duration_since(std::time::UNIX_EPOCH)
267 .expect("System time is before UNIX epoch")
268 .as_nanos()
269 ));
270 std::fs::create_dir_all(&root).expect("Failed to create test root directory");
271 let mut bin = root.clone();
272 bin.push("bin");
273 std::fs::create_dir_all(&bin).expect("Failed to create test bin directory");
274 let mut script = bin.clone();
275 script.push("pacman");
276 let body = r#"#!/usr/bin/env bash
277set -e
278if [[ "$1" == "-Si" ]]; then
279 # Print two blocks, one for foo, one unrelated
280 cat <<EOF
281Name : foo
282Version : 1.2.3
283Architecture : x86_64
284Repository : core
285Description : hello
286
287Name : other
288Version : 9.9.9
289Architecture : any
290Repository : extra
291Description : nope
292EOF
293 exit 0
294fi
295exit 0
296"#;
297 std::fs::write(&script, body).expect("Failed to write test pacman script");
298 #[cfg(unix)]
299 {
300 use std::os::unix::fs::PermissionsExt;
301 let mut perm = std::fs::metadata(&script)
302 .expect("Failed to read test pacman script metadata")
303 .permissions();
304 perm.set_mode(0o755);
305 std::fs::set_permissions(&script, perm)
306 .expect("Failed to set test pacman script permissions");
307 }
308 let new_path = format!("{}:{old_path}", bin.to_string_lossy());
309 unsafe { std::env::set_var("PATH", &new_path) };
310
311 let mut path: std::path::PathBuf = std::env::temp_dir();
313 path.push("pacsea_enrich_test.json");
314 crate::index::save_to_disk(&path);
315
316 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<()>();
317 super::request_enrich_for(path.clone(), tx, vec!["foo".into(), "foo".into()]);
318 let notified = tokio::time::timeout(std::time::Duration::from_millis(500), rx.recv())
320 .await
321 .ok()
322 .flatten()
323 .is_some();
324 assert!(notified);
325
326 let all = crate::index::all_official();
328 let pkg = all
329 .iter()
330 .find(|p| p.name == "foo")
331 .expect("package 'foo' should exist in test data");
332 assert_eq!(pkg.version, "1.2.3");
333 assert_eq!(pkg.description, "hello");
334 match &pkg.source {
335 crate::state::Source::Official { repo, arch } => {
336 assert_eq!(repo, "core");
337 assert_eq!(arch, "x86_64");
338 }
339 crate::state::Source::Aur => panic!("expected official"),
340 }
341
342 unsafe { std::env::set_var("PATH", &old_path) };
344 let _ = std::fs::remove_file(&path);
345 let _ = std::fs::remove_dir_all(&root);
346 }
347}