1use std::collections::HashSet;
11
12use crate::deps::parse::parse_dep_spec;
13
14#[allow(clippy::case_sensitive_file_extension_comparisons)]
37#[must_use]
38pub fn parse_pkgbuild_deps(pkgbuild: &str) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
39 let mut depends = Vec::new();
40 let mut makedepends = Vec::new();
41 let mut checkdepends = Vec::new();
42 let mut optdepends = Vec::new();
43
44 let mut seen_depends = HashSet::new();
46 let mut seen_makedepends = HashSet::new();
47 let mut seen_checkdepends = HashSet::new();
48 let mut seen_optdepends = HashSet::new();
49
50 let lines: Vec<&str> = pkgbuild.lines().collect();
51 let mut i = 0;
52
53 while i < lines.len() {
54 let line = lines[i].trim();
55 i += 1;
56
57 if line.is_empty() || line.starts_with('#') {
58 continue;
59 }
60
61 if let Some((key, value)) = line.split_once('=') {
63 let key = key.trim();
64 let value = value.trim();
65
66 let base_key = key.strip_suffix('+').map_or(key, |stripped| stripped);
68
69 if !matches!(
71 base_key,
72 "depends" | "makedepends" | "checkdepends" | "optdepends"
73 ) {
74 continue;
75 }
76
77 if value.starts_with('(') {
79 let deps = find_matching_closing_paren(value).map_or_else(
80 || {
81 let mut array_lines = Vec::new();
86 while i < lines.len() {
88 let next_line = lines[i].trim();
89 i += 1;
90
91 if next_line.is_empty() || next_line.starts_with('#') {
93 continue;
94 }
95
96 if next_line == ")" {
98 break;
99 }
100
101 if let Some(paren_pos) = next_line.find(')') {
103 let content_before_paren = &next_line[..paren_pos].trim();
105 if !content_before_paren.is_empty() {
106 array_lines.push((*content_before_paren).to_string());
107 }
108 break;
109 }
110
111 array_lines.push(next_line.to_string());
113 }
114
115 let array_content = array_lines
118 .iter()
119 .map(|s| s.trim())
120 .filter(|s| !s.is_empty())
121 .collect::<Vec<_>>()
122 .join(" ");
123 parse_array_content(&array_content)
124 },
125 |closing_paren_pos| {
126 let array_content = &value[1..closing_paren_pos];
128 parse_array_content(array_content)
129 },
130 );
131
132 let filtered_deps: Vec<String> = deps
134 .into_iter()
135 .filter_map(|dep| {
136 let dep_trimmed = dep.trim();
137 if dep_trimmed.is_empty() {
138 return None;
139 }
140
141 if is_valid_dependency(dep_trimmed) {
142 Some(dep_trimmed.to_string())
143 } else {
144 None
145 }
146 })
147 .collect();
148
149 match base_key {
152 "depends" => {
153 for dep in filtered_deps {
154 if seen_depends.insert(dep.clone()) {
155 depends.push(dep);
156 }
157 }
158 }
159 "makedepends" => {
160 for dep in filtered_deps {
161 if seen_makedepends.insert(dep.clone()) {
162 makedepends.push(dep);
163 }
164 }
165 }
166 "checkdepends" => {
167 for dep in filtered_deps {
168 if seen_checkdepends.insert(dep.clone()) {
169 checkdepends.push(dep);
170 }
171 }
172 }
173 "optdepends" => {
174 for dep in filtered_deps {
175 if seen_optdepends.insert(dep.clone()) {
176 optdepends.push(dep);
177 }
178 }
179 }
180 _ => {}
181 }
182 }
183 }
184 }
185
186 (depends, makedepends, checkdepends, optdepends)
187}
188
189#[allow(clippy::case_sensitive_file_extension_comparisons)]
212#[must_use]
213pub fn parse_pkgbuild_conflicts(pkgbuild: &str) -> Vec<String> {
214 let mut conflicts = Vec::new();
215 let mut seen = HashSet::new();
216
217 let lines: Vec<&str> = pkgbuild.lines().collect();
218 let mut i = 0;
219
220 while i < lines.len() {
221 let line = lines[i].trim();
222 i += 1;
223
224 if line.is_empty() || line.starts_with('#') {
225 continue;
226 }
227
228 if let Some((key, value)) = line.split_once('=') {
230 let key = key.trim();
231 let value = value.trim();
232
233 let base_key = key.strip_suffix('+').map_or(key, |stripped| stripped);
235
236 if base_key != "conflicts" {
238 continue;
239 }
240
241 if value.starts_with('(') {
243 let conflict_deps = find_matching_closing_paren(value).map_or_else(
244 || {
245 let mut array_lines = Vec::new();
250 while i < lines.len() {
252 let next_line = lines[i].trim();
253 i += 1;
254
255 if next_line.is_empty() || next_line.starts_with('#') {
257 continue;
258 }
259
260 if next_line == ")" {
262 break;
263 }
264
265 if let Some(paren_pos) = next_line.find(')') {
267 let content_before_paren = &next_line[..paren_pos].trim();
269 if !content_before_paren.is_empty() {
270 array_lines.push((*content_before_paren).to_string());
271 }
272 break;
273 }
274
275 array_lines.push(next_line.to_string());
277 }
278
279 let array_content = array_lines
281 .iter()
282 .map(|s| s.trim())
283 .filter(|s| !s.is_empty())
284 .collect::<Vec<_>>()
285 .join(" ");
286 parse_array_content(&array_content)
287 },
288 |closing_paren_pos| {
289 let array_content = &value[1..closing_paren_pos];
291 parse_array_content(array_content)
292 },
293 );
294
295 let filtered_conflicts: Vec<String> = conflict_deps
297 .into_iter()
298 .filter_map(|conflict| {
299 let conflict_trimmed = conflict.trim();
300 if conflict_trimmed.is_empty() {
301 return None;
302 }
303
304 if is_valid_dependency(conflict_trimmed) {
305 let spec = parse_dep_spec(conflict_trimmed);
308 if !spec.name.is_empty() && seen.insert(spec.name.clone()) {
309 Some(spec.name)
310 } else {
311 None
312 }
313 } else {
314 None
315 }
316 })
317 .collect();
318
319 conflicts.extend(filtered_conflicts);
321 }
322 }
323 }
324
325 conflicts
326}
327
328fn find_matching_closing_paren(s: &str) -> Option<usize> {
339 let mut depth = 0;
340 let mut in_quotes = false;
341 let mut quote_char = '\0';
342
343 for (pos, ch) in s.char_indices() {
344 match ch {
345 '\'' | '"' => {
346 if !in_quotes {
347 in_quotes = true;
348 quote_char = ch;
349 } else if ch == quote_char {
350 in_quotes = false;
351 quote_char = '\0';
352 }
353 }
354 '(' if !in_quotes => {
355 depth += 1;
356 }
357 ')' if !in_quotes => {
358 depth -= 1;
359 if depth == 0 {
360 return Some(pos);
361 }
362 }
363 _ => {}
364 }
365 }
366 None
367}
368
369fn parse_array_content(content: &str) -> Vec<String> {
381 let mut deps = Vec::new();
382 let mut in_quotes = false;
383 let mut quote_char = '\0';
384 let mut current = String::new();
385
386 for ch in content.chars() {
387 match ch {
388 '\'' | '"' => {
389 if !in_quotes {
390 in_quotes = true;
391 quote_char = ch;
392 } else if ch == quote_char {
393 if !current.is_empty() {
394 deps.push(current.clone());
395 current.clear();
396 }
397 in_quotes = false;
398 quote_char = '\0';
399 } else {
400 current.push(ch);
401 }
402 }
403 _ if in_quotes => {
404 current.push(ch);
405 }
406 ch if ch.is_whitespace() => {
407 if !current.is_empty() {
409 deps.push(current.clone());
410 current.clear();
411 }
412 }
413 _ => {
414 current.push(ch);
416 }
417 }
418 }
419
420 if !current.is_empty() {
422 deps.push(current);
423 }
424
425 deps
426}
427
428fn is_valid_dependency(dep: &str) -> bool {
443 let dep_lower = dep.to_lowercase();
445 if std::path::Path::new(&dep_lower)
446 .extension()
447 .is_some_and(|ext| ext.eq_ignore_ascii_case("so"))
448 || dep_lower.contains(".so.")
449 || dep_lower.contains(".so=")
450 {
451 return false;
452 }
453
454 if dep.ends_with(')') {
458 if dep.contains(">=") || dep.contains("<=") || dep.contains("==") {
461 return false;
463 }
464 return false;
466 }
467
468 let Some(first_char) = dep.chars().next() else {
471 return false;
472 };
473 if !first_char.is_alphanumeric() && first_char != '_' {
474 return false;
475 }
476
477 if dep.len() < 2 {
479 return false;
480 }
481
482 let has_valid_chars = dep
485 .chars()
486 .any(|c| c.is_alphanumeric() || c == '-' || c == '_');
487 if !has_valid_chars {
488 return false;
489 }
490
491 true
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 #[test]
501 fn test_parse_pkgbuild_deps_basic() {
502 let pkgbuild = r"
503pkgname=test-package
504pkgver=1.0.0
505depends=('foo' 'bar>=1.2')
506makedepends=('make' 'gcc')
507";
508
509 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
510
511 assert_eq!(depends.len(), 2);
512 assert!(depends.contains(&"foo".to_string()));
513 assert!(depends.contains(&"bar>=1.2".to_string()));
514
515 assert_eq!(makedepends.len(), 2);
516 assert!(makedepends.contains(&"make".to_string()));
517 assert!(makedepends.contains(&"gcc".to_string()));
518
519 assert_eq!(checkdepends.len(), 0);
520 assert_eq!(optdepends.len(), 0);
521 }
522
523 #[test]
524 fn test_parse_pkgbuild_deps_append() {
525 let pkgbuild = r#"
526pkgname=test-package
527pkgver=1.0.0
528package() {
529 depends+=(foo bar)
530 cd $_pkgname
531 make DESTDIR="$pkgdir" PREFIX=/usr install
532}
533"#;
534
535 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
536
537 assert_eq!(depends.len(), 2);
538 assert!(depends.contains(&"foo".to_string()));
539 assert!(depends.contains(&"bar".to_string()));
540
541 assert_eq!(makedepends.len(), 0);
542 assert_eq!(checkdepends.len(), 0);
543 assert_eq!(optdepends.len(), 0);
544 }
545
546 #[test]
547 fn test_parse_pkgbuild_deps_unquoted() {
548 let pkgbuild = r"
549pkgname=test-package
550depends=(foo bar libcairo.so libdbus-1.so)
551";
552
553 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
554
555 assert_eq!(depends.len(), 2);
557 assert!(depends.contains(&"foo".to_string()));
558 assert!(depends.contains(&"bar".to_string()));
559
560 assert_eq!(makedepends.len(), 0);
561 assert_eq!(checkdepends.len(), 0);
562 assert_eq!(optdepends.len(), 0);
563 }
564
565 #[test]
566 fn test_parse_pkgbuild_deps_multiline() {
567 let pkgbuild = r"
568pkgname=test-package
569depends=(
570 'foo'
571 'bar>=1.2'
572 'baz'
573)
574";
575
576 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
577
578 assert_eq!(depends.len(), 3);
579 assert!(depends.contains(&"foo".to_string()));
580 assert!(depends.contains(&"bar>=1.2".to_string()));
581 assert!(depends.contains(&"baz".to_string()));
582
583 assert_eq!(makedepends.len(), 0);
584 assert_eq!(checkdepends.len(), 0);
585 assert_eq!(optdepends.len(), 0);
586 }
587
588 #[test]
589 fn test_parse_pkgbuild_deps_makedepends_append() {
590 let pkgbuild = r"
591pkgname=test-package
592build() {
593 makedepends+=(cmake ninja)
594 cmake -B build
595}
596";
597
598 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
599
600 assert_eq!(makedepends.len(), 2);
601 assert!(makedepends.contains(&"cmake".to_string()));
602 assert!(makedepends.contains(&"ninja".to_string()));
603
604 assert_eq!(depends.len(), 0);
605 assert_eq!(checkdepends.len(), 0);
606 assert_eq!(optdepends.len(), 0);
607 }
608
609 #[test]
610 fn test_parse_pkgbuild_deps_jujutsu_git_scenario() {
611 let pkgbuild = r"
612pkgname=jujutsu-git
613pkgver=0.1.0
614pkgdesc=Git-compatible VCS that is both simple and powerful
615url=https://github.com/martinvonz/jj
616license=(Apache-2.0)
617arch=(i686 x86_64 armv6h armv7h)
618depends=(
619 glibc
620 libc.so
621 libm.so
622)
623makedepends=(
624 libgit2
625 libgit2.so
626 libssh2
627 libssh2.so)
628 openssh
629 git)
630cargo
631checkdepends=()
632optdepends=()
633source=($pkgname::git+$url)
634";
635
636 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
637
638 assert_eq!(depends.len(), 1);
640 assert!(depends.contains(&"glibc".to_string()));
641
642 assert_eq!(makedepends.len(), 2);
646 assert!(makedepends.contains(&"libgit2".to_string()));
647 assert!(makedepends.contains(&"libssh2".to_string()));
648
649 assert_eq!(checkdepends.len(), 0);
650 assert_eq!(optdepends.len(), 0);
651 }
652
653 #[test]
654 fn test_parse_pkgbuild_deps_ignore_other_fields() {
655 let pkgbuild = r"
656pkgname=test-package
657pkgver=1.0.0
658pkgdesc=Test package description
659url=https://example.com
660license=(MIT)
661arch=(x86_64)
662source=($pkgname-$pkgver.tar.gz)
663depends=(foo bar)
664makedepends=(make)
665";
666
667 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
668
669 assert_eq!(depends.len(), 2);
671 assert!(depends.contains(&"foo".to_string()));
672 assert!(depends.contains(&"bar".to_string()));
673
674 assert_eq!(makedepends.len(), 1);
675 assert!(makedepends.contains(&"make".to_string()));
676
677 assert_eq!(checkdepends.len(), 0);
678 assert_eq!(optdepends.len(), 0);
679 }
680
681 #[test]
682 fn test_parse_pkgbuild_deps_filter_invalid_names() {
683 let pkgbuild = r"
685depends=('valid-package' 'invalid)' '=invalid' 'a' 'valid>=1.0')
686";
687
688 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
689
690 assert_eq!(depends.len(), 2);
696 assert!(depends.contains(&"valid-package".to_string()));
697 assert!(depends.contains(&"valid>=1.0".to_string()));
698
699 assert_eq!(makedepends.len(), 0);
700 assert_eq!(checkdepends.len(), 0);
701 assert_eq!(optdepends.len(), 0);
702 }
703
704 #[test]
705 fn test_parse_pkgbuild_deps_deduplicates() {
706 let pkgbuild = r"
707depends=('foo' 'bar' 'foo' 'baz' 'bar')
708";
709
710 let (depends, _, _, _) = parse_pkgbuild_deps(pkgbuild);
711 assert_eq!(depends.len(), 3, "Should deduplicate dependencies");
712 assert!(depends.contains(&"foo".to_string()));
713 assert!(depends.contains(&"bar".to_string()));
714 assert!(depends.contains(&"baz".to_string()));
715 }
716
717 #[test]
718 fn test_parse_pkgbuild_deps_empty() {
719 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps("");
720 assert_eq!(depends.len(), 0);
721 assert_eq!(makedepends.len(), 0);
722 assert_eq!(checkdepends.len(), 0);
723 assert_eq!(optdepends.len(), 0);
724 }
725
726 #[test]
727 fn test_parse_pkgbuild_deps_comments_and_blank_lines() {
728 let pkgbuild = r"
729# This is a comment
730pkgname=test-package
731
732depends=(foo bar)
733# Another comment
734makedepends=(make)
735";
736
737 let (depends, makedepends, _, _) = parse_pkgbuild_deps(pkgbuild);
738 assert_eq!(depends.len(), 2);
739 assert!(depends.contains(&"foo".to_string()));
740 assert!(depends.contains(&"bar".to_string()));
741 assert_eq!(makedepends.len(), 1);
742 assert!(makedepends.contains(&"make".to_string()));
743 }
744
745 #[test]
746 fn test_parse_pkgbuild_deps_mixed_quoted_unquoted() {
747 let pkgbuild = r"
748depends=('quoted' unquoted 'another-quoted' unquoted2)
749";
750
751 let (depends, _, _, _) = parse_pkgbuild_deps(pkgbuild);
752 assert_eq!(depends.len(), 4);
753 assert!(depends.contains(&"quoted".to_string()));
754 assert!(depends.contains(&"unquoted".to_string()));
755 assert!(depends.contains(&"another-quoted".to_string()));
756 assert!(depends.contains(&"unquoted2".to_string()));
757 }
758
759 #[test]
762 fn test_parse_pkgbuild_conflicts_basic() {
763 let pkgbuild = r"
764pkgname=jujutsu-git
765pkgver=0.1.0
766conflicts=('jujutsu')
767";
768
769 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
770
771 assert_eq!(conflicts.len(), 1);
772 assert!(conflicts.contains(&"jujutsu".to_string()));
773 }
774
775 #[test]
776 fn test_parse_pkgbuild_conflicts_multiline() {
777 let pkgbuild = r"
778pkgname=pacsea-git
779pkgver=0.1.0
780conflicts=(
781 'pacsea'
782 'pacsea-bin'
783)
784";
785
786 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
787
788 assert_eq!(conflicts.len(), 2);
789 assert!(conflicts.contains(&"pacsea".to_string()));
790 assert!(conflicts.contains(&"pacsea-bin".to_string()));
791 }
792
793 #[test]
794 fn test_parse_pkgbuild_conflicts_with_versions() {
795 let pkgbuild = r"
796pkgname=test-package
797conflicts=('old-pkg<2.0' 'new-pkg>=3.0')
798";
799
800 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
801
802 assert_eq!(conflicts.len(), 2);
803 assert!(conflicts.contains(&"old-pkg".to_string()));
804 assert!(conflicts.contains(&"new-pkg".to_string()));
805 }
806
807 #[test]
808 fn test_parse_pkgbuild_conflicts_filter_so() {
809 let pkgbuild = r"
810pkgname=test-package
811conflicts=('foo' 'libcairo.so' 'bar' 'libdbus-1.so=1-64')
812";
813
814 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
815
816 assert_eq!(conflicts.len(), 2);
818 assert!(conflicts.contains(&"foo".to_string()));
819 assert!(conflicts.contains(&"bar".to_string()));
820 }
821
822 #[test]
823 fn test_parse_pkgbuild_conflicts_deduplicates() {
824 let pkgbuild = r"
825conflicts=('pkg1' 'pkg2' 'pkg1' 'pkg3')
826";
827
828 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
829 assert_eq!(conflicts.len(), 3, "Should deduplicate conflicts");
830 assert!(conflicts.contains(&"pkg1".to_string()));
831 assert!(conflicts.contains(&"pkg2".to_string()));
832 assert!(conflicts.contains(&"pkg3".to_string()));
833 }
834
835 #[test]
836 fn test_parse_pkgbuild_conflicts_empty() {
837 let conflicts = parse_pkgbuild_conflicts("");
838 assert!(conflicts.is_empty());
839 }
840}