arch_toolkit/deps/
srcinfo.rs1use std::collections::HashSet;
7
8use crate::deps::parse::parse_dep_spec;
9use crate::error::Result;
10use crate::types::SrcinfoData;
11
12#[cfg(feature = "aur")]
13use crate::aur::utils::percent_encode;
14
15#[allow(clippy::case_sensitive_file_extension_comparisons)]
30#[must_use]
31pub fn parse_srcinfo_deps(srcinfo: &str) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
32 let mut depends = Vec::new();
33 let mut makedepends = Vec::new();
34 let mut checkdepends = Vec::new();
35 let mut optdepends = Vec::new();
36
37 let mut seen_depends = HashSet::new();
39 let mut seen_makedepends = HashSet::new();
40 let mut seen_checkdepends = HashSet::new();
41 let mut seen_optdepends = HashSet::new();
42
43 for line in srcinfo.lines() {
44 let line = line.trim();
45 if line.is_empty() || line.starts_with('#') {
46 continue;
47 }
48
49 if let Some((key, value)) = line.split_once('=') {
51 let key = key.trim();
52 let value = value.trim();
53
54 let value_lower = value.to_lowercase();
56 if value_lower.ends_with(".so")
57 || value_lower.contains(".so.")
58 || value_lower.contains(".so=")
59 {
60 continue;
61 }
62
63 let base_key = key
65 .find('_')
66 .map_or(key, |underscore_pos| &key[..underscore_pos]);
67
68 match base_key {
69 "depends" => {
70 if seen_depends.insert(value.to_string()) {
71 depends.push(value.to_string());
72 }
73 }
74 "makedepends" => {
75 if seen_makedepends.insert(value.to_string()) {
76 makedepends.push(value.to_string());
77 }
78 }
79 "checkdepends" => {
80 if seen_checkdepends.insert(value.to_string()) {
81 checkdepends.push(value.to_string());
82 }
83 }
84 "optdepends" => {
85 if seen_optdepends.insert(value.to_string()) {
86 optdepends.push(value.to_string());
87 }
88 }
89 _ => {}
90 }
91 }
92 }
93
94 (depends, makedepends, checkdepends, optdepends)
95}
96
97#[allow(clippy::case_sensitive_file_extension_comparisons)]
111#[must_use]
112pub fn parse_srcinfo_conflicts(srcinfo: &str) -> Vec<String> {
113 let mut conflicts = Vec::new();
114 let mut seen = HashSet::new();
115
116 for line in srcinfo.lines() {
117 let line = line.trim();
118 if line.is_empty() || line.starts_with('#') {
119 continue;
120 }
121
122 if let Some((key, value)) = line.split_once('=') {
124 let key = key.trim();
125 let value = value.trim();
126
127 let base_key = key
129 .find('_')
130 .map_or(key, |underscore_pos| &key[..underscore_pos]);
131
132 if base_key == "conflicts" {
133 let value_lower = value.to_lowercase();
135 if value_lower.ends_with(".so")
136 || value_lower.contains(".so.")
137 || value_lower.contains(".so=")
138 {
139 continue;
140 }
141 let spec = parse_dep_spec(value);
143 if !spec.name.is_empty() && seen.insert(spec.name.clone()) {
144 conflicts.push(spec.name);
145 }
146 }
147 }
148 }
149
150 conflicts
151}
152
153#[must_use]
169pub fn parse_srcinfo(content: &str) -> SrcinfoData {
170 let mut data = SrcinfoData::default();
171 let mut pkgname_found = false;
172
173 let (depends, makedepends, checkdepends, optdepends) = parse_srcinfo_deps(content);
175 data.depends = depends;
176 data.makedepends = makedepends;
177 data.checkdepends = checkdepends;
178 data.optdepends = optdepends;
179 data.conflicts = parse_srcinfo_conflicts(content);
180
181 let mut seen_provides = HashSet::new();
183 let mut seen_replaces = HashSet::new();
184
185 for line in content.lines() {
186 let line = line.trim();
187 if line.is_empty() || line.starts_with('#') {
188 continue;
189 }
190
191 if let Some((key, value)) = line.split_once('=') {
192 let key = key.trim();
193 let value = value.trim();
194
195 let base_key = key
197 .find('_')
198 .map_or(key, |underscore_pos| &key[..underscore_pos]);
199
200 match base_key {
201 "pkgbase" => {
202 if data.pkgbase.is_empty() {
203 data.pkgbase = value.to_string();
204 }
205 }
206 "pkgname" => {
207 if !pkgname_found {
209 data.pkgname = value.to_string();
210 pkgname_found = true;
211 }
212 }
213 "pkgver" => {
214 if data.pkgver.is_empty() {
215 data.pkgver = value.to_string();
216 }
217 }
218 "pkgrel" => {
219 if data.pkgrel.is_empty() {
220 data.pkgrel = value.to_string();
221 }
222 }
223 "provides" => {
224 if seen_provides.insert(value.to_string()) {
225 data.provides.push(value.to_string());
226 }
227 }
228 "replaces" => {
229 if seen_replaces.insert(value.to_string()) {
230 data.replaces.push(value.to_string());
231 }
232 }
233 _ => {}
234 }
235 }
236 }
237
238 data
239}
240
241#[cfg(feature = "aur")]
262pub async fn fetch_srcinfo(client: &reqwest::Client, name: &str) -> Result<String> {
263 use crate::error::ArchToolkitError;
264
265 let url = format!(
266 "https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h={}",
267 percent_encode(name)
268 );
269 tracing::debug!("Fetching .SRCINFO from: {}", url);
270
271 let response = client
272 .get(&url)
273 .send()
274 .await
275 .map_err(ArchToolkitError::Network)?;
276
277 if !response.status().is_success() {
278 return Err(ArchToolkitError::InvalidInput(format!(
279 "HTTP request failed with status: {}",
280 response.status()
281 )));
282 }
283
284 let text = response.text().await.map_err(ArchToolkitError::Network)?;
285
286 if text.trim().is_empty() {
287 return Err(ArchToolkitError::EmptyInput {
288 field: "srcinfo_content".to_string(),
289 message: "Empty .SRCINFO content".to_string(),
290 });
291 }
292
293 if text.trim_start().starts_with("<html") || text.trim_start().starts_with("<!DOCTYPE") {
295 return Err(ArchToolkitError::Parse(
296 "Received HTML error page instead of .SRCINFO".to_string(),
297 ));
298 }
299
300 if !text.contains("pkgbase =") && !text.contains("pkgname =") {
302 return Err(ArchToolkitError::Parse(
303 "Response does not appear to be valid .SRCINFO format".to_string(),
304 ));
305 }
306
307 Ok(text)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_parse_srcinfo_deps() {
316 let srcinfo = r"
317pkgbase = test-package
318pkgname = test-package
319pkgver = 1.0.0
320pkgrel = 1
321depends = foo
322depends = bar>=1.2.3
323makedepends = make
324makedepends = gcc
325checkdepends = check
326optdepends = optional: optional-package
327depends = libfoo.so=1-64
328";
329
330 let (depends, makedepends, checkdepends, optdepends) = parse_srcinfo_deps(srcinfo);
331
332 assert_eq!(depends.len(), 2);
334 assert!(depends.contains(&"foo".to_string()));
335 assert!(depends.contains(&"bar>=1.2.3".to_string()));
336
337 assert_eq!(makedepends.len(), 2);
339 assert!(makedepends.contains(&"make".to_string()));
340 assert!(makedepends.contains(&"gcc".to_string()));
341
342 assert_eq!(checkdepends.len(), 1);
344 assert!(checkdepends.contains(&"check".to_string()));
345
346 assert_eq!(optdepends.len(), 1);
348 assert!(optdepends.contains(&"optional: optional-package".to_string()));
349 }
350
351 #[test]
352 fn test_parse_srcinfo_deps_deduplicates() {
353 let srcinfo = r"
354depends = glibc
355depends = gtk3
356depends = glibc
357depends = nss
358";
359
360 let (depends, _, _, _) = parse_srcinfo_deps(srcinfo);
361 assert_eq!(depends.len(), 3, "Should deduplicate dependencies");
362 assert!(depends.contains(&"glibc".to_string()));
363 assert!(depends.contains(&"gtk3".to_string()));
364 assert!(depends.contains(&"nss".to_string()));
365 }
366
367 #[test]
368 fn test_parse_srcinfo_deps_arch_specific() {
369 let srcinfo = r"
370depends = common-dep
371depends_x86_64 = arch-specific-dep
372depends_aarch64 = arm-dep
373";
374
375 let (depends, _, _, _) = parse_srcinfo_deps(srcinfo);
376 assert!(depends.contains(&"common-dep".to_string()));
378 assert!(depends.contains(&"arch-specific-dep".to_string()));
379 assert!(depends.contains(&"arm-dep".to_string()));
380 }
381
382 #[test]
383 fn test_parse_srcinfo_conflicts() {
384 let srcinfo = r"
385pkgbase = test-package
386pkgname = test-package
387pkgver = 1.0.0
388pkgrel = 1
389conflicts = conflicting-pkg1
390conflicts = conflicting-pkg2>=2.0
391conflicts = libfoo.so=1-64
392";
393
394 let conflicts = parse_srcinfo_conflicts(srcinfo);
395
396 assert_eq!(conflicts.len(), 2);
398 assert!(conflicts.contains(&"conflicting-pkg1".to_string()));
399 assert!(conflicts.contains(&"conflicting-pkg2".to_string()));
400 }
401
402 #[test]
403 fn test_parse_srcinfo_conflicts_empty() {
404 let srcinfo = r"
405pkgbase = test-package
406pkgname = test-package
407pkgver = 1.0.0
408";
409
410 let conflicts = parse_srcinfo_conflicts(srcinfo);
411 assert!(conflicts.is_empty());
412 }
413
414 #[test]
415 fn test_parse_srcinfo_conflicts_deduplicates() {
416 let srcinfo = r"
417conflicts = pkg1
418conflicts = pkg2
419conflicts = pkg1
420conflicts = pkg3
421";
422
423 let conflicts = parse_srcinfo_conflicts(srcinfo);
424 assert_eq!(conflicts.len(), 3, "Should deduplicate conflicts");
425 assert!(conflicts.contains(&"pkg1".to_string()));
426 assert!(conflicts.contains(&"pkg2".to_string()));
427 assert!(conflicts.contains(&"pkg3".to_string()));
428 }
429
430 #[test]
431 fn test_parse_srcinfo_full() {
432 let srcinfo = r"
433pkgbase = test-package
434pkgname = test-package
435pkgver = 1.0.0
436pkgrel = 1
437depends = glibc
438depends = python>=3.12
439makedepends = make
440checkdepends = check
441optdepends = optional: optional-package
442conflicts = conflicting-pkg
443provides = provided-pkg
444replaces = replaced-pkg
445";
446
447 let data = parse_srcinfo(srcinfo);
448
449 assert_eq!(data.pkgbase, "test-package");
450 assert_eq!(data.pkgname, "test-package");
451 assert_eq!(data.pkgver, "1.0.0");
452 assert_eq!(data.pkgrel, "1");
453 assert_eq!(data.depends.len(), 2);
454 assert!(data.depends.contains(&"glibc".to_string()));
455 assert!(data.depends.contains(&"python>=3.12".to_string()));
456 assert_eq!(data.makedepends.len(), 1);
457 assert!(data.makedepends.contains(&"make".to_string()));
458 assert_eq!(data.checkdepends.len(), 1);
459 assert!(data.checkdepends.contains(&"check".to_string()));
460 assert_eq!(data.optdepends.len(), 1);
461 assert!(
462 data.optdepends
463 .contains(&"optional: optional-package".to_string())
464 );
465 assert_eq!(data.conflicts.len(), 1);
466 assert!(data.conflicts.contains(&"conflicting-pkg".to_string()));
467 assert_eq!(data.provides.len(), 1);
468 assert!(data.provides.contains(&"provided-pkg".to_string()));
469 assert_eq!(data.replaces.len(), 1);
470 assert!(data.replaces.contains(&"replaced-pkg".to_string()));
471 }
472
473 #[test]
474 fn test_parse_srcinfo_split_packages() {
475 let srcinfo = r"
476pkgbase = split-package
477pkgname = split-package-base
478pkgname = split-package-gui
479pkgver = 1.0.0
480pkgrel = 1
481";
482
483 let data = parse_srcinfo(srcinfo);
484 assert_eq!(data.pkgname, "split-package-base");
486 assert_eq!(data.pkgbase, "split-package");
487 }
488
489 #[test]
490 fn test_parse_srcinfo_comments_and_blank_lines() {
491 let srcinfo = r"
492# This is a comment
493pkgbase = test-package
494
495pkgname = test-package
496# Another comment
497pkgver = 1.0.0
498";
499
500 let data = parse_srcinfo(srcinfo);
501 assert_eq!(data.pkgbase, "test-package");
502 assert_eq!(data.pkgname, "test-package");
503 assert_eq!(data.pkgver, "1.0.0");
504 }
505
506 #[test]
507 fn test_parse_srcinfo_empty() {
508 let data = parse_srcinfo("");
509 assert_eq!(data.pkgbase, "");
510 assert_eq!(data.pkgname, "");
511 assert_eq!(data.pkgver, "");
512 assert_eq!(data.pkgrel, "");
513 assert!(data.depends.is_empty());
514 assert!(data.makedepends.is_empty());
515 assert!(data.checkdepends.is_empty());
516 assert!(data.optdepends.is_empty());
517 assert!(data.conflicts.is_empty());
518 assert!(data.provides.is_empty());
519 assert!(data.replaces.is_empty());
520 }
521
522 #[test]
523 fn test_parse_srcinfo_malformed() {
524 let srcinfo = r"
526pkgbase test-package
527invalid line
528";
529
530 let data = parse_srcinfo(srcinfo);
531 assert_eq!(data.pkgbase, "");
533 }
534}