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 lines: Vec<&str> = pkgbuild.lines().collect();
40 let mut index = 0;
41 let mut fields = DependencyFields::default();
42
43 while index < lines.len() {
44 let line = lines[index].trim();
45 index += 1;
46 let Some((key, value)) = dependency_declaration(line) else {
47 continue;
48 };
49 let values = parse_declared_array(value, &lines, &mut index);
50 fields.extend(key, values);
51 }
52
53 fields.into_parts()
54}
55
56#[derive(Clone, Copy)]
67enum DependencyField {
68 Runtime,
70 Make,
72 Check,
74 Optional,
76}
77
78#[derive(Default)]
90struct DependencyFields {
91 depends: (Vec<String>, HashSet<String>),
93 makedepends: (Vec<String>, HashSet<String>),
95 checkdepends: (Vec<String>, HashSet<String>),
97 optdepends: (Vec<String>, HashSet<String>),
99}
100
101impl DependencyFields {
102 fn extend(&mut self, field: DependencyField, values: Vec<String>) {
114 let destination = match field {
115 DependencyField::Runtime => &mut self.depends,
116 DependencyField::Make => &mut self.makedepends,
117 DependencyField::Check => &mut self.checkdepends,
118 DependencyField::Optional => &mut self.optdepends,
119 };
120 for value in values {
121 let value = value.trim();
122 if !value.is_empty() && is_valid_dependency(value) && destination.1.insert(value.into())
123 {
124 destination.0.push(value.into());
125 }
126 }
127 }
128
129 fn into_parts(self) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
140 (
141 self.depends.0,
142 self.makedepends.0,
143 self.checkdepends.0,
144 self.optdepends.0,
145 )
146 }
147}
148
149fn dependency_declaration(line: &str) -> Option<(DependencyField, &str)> {
160 if line.is_empty() || line.starts_with('#') {
161 return None;
162 }
163 let (key, value) = line.split_once('=')?;
164 let field = match key.trim().strip_suffix('+').unwrap_or_else(|| key.trim()) {
165 "depends" => DependencyField::Runtime,
166 "makedepends" => DependencyField::Make,
167 "checkdepends" => DependencyField::Check,
168 "optdepends" => DependencyField::Optional,
169 _ => return None,
170 };
171 value
172 .trim()
173 .starts_with('(')
174 .then_some((field, value.trim()))
175}
176
177fn parse_declared_array(value: &str, lines: &[&str], index: &mut usize) -> Vec<String> {
190 if let Some(closing) = find_matching_closing_paren(value) {
191 return parse_array_content(&value[1..closing]);
192 }
193
194 let mut parts = Vec::new();
195 let first = value[1..].trim();
196 if !first.is_empty() && !first.starts_with('#') {
197 parts.push(first.to_string());
198 }
199 while *index < lines.len() {
200 let line = lines[*index].trim();
201 *index += 1;
202 if line.is_empty() || line.starts_with('#') {
203 continue;
204 }
205 if line == ")" {
206 break;
207 }
208 if let Some(closing) = line.find(')') {
209 let content = line[..closing].trim();
210 if !content.is_empty() {
211 parts.push(content.to_string());
212 }
213 break;
214 }
215 parts.push(line.to_string());
216 }
217 parse_array_content(&parts.join(" "))
218}
219
220#[allow(clippy::case_sensitive_file_extension_comparisons)]
243#[must_use]
244pub fn parse_pkgbuild_conflicts(pkgbuild: &str) -> Vec<String> {
245 let mut conflicts = Vec::new();
246 let mut seen = HashSet::new();
247
248 let lines: Vec<&str> = pkgbuild.lines().collect();
249 let mut i = 0;
250
251 while i < lines.len() {
252 let line = lines[i].trim();
253 i += 1;
254
255 if line.is_empty() || line.starts_with('#') {
256 continue;
257 }
258
259 if let Some((key, value)) = line.split_once('=') {
261 let key = key.trim();
262 let value = value.trim();
263
264 let base_key = key.strip_suffix('+').map_or(key, |stripped| stripped);
266
267 if base_key != "conflicts" {
269 continue;
270 }
271
272 if value.starts_with('(') {
274 let conflict_deps = find_matching_closing_paren(value).map_or_else(
275 || {
276 let mut array_lines = Vec::new();
281 while i < lines.len() {
283 let next_line = lines[i].trim();
284 i += 1;
285
286 if next_line.is_empty() || next_line.starts_with('#') {
288 continue;
289 }
290
291 if next_line == ")" {
293 break;
294 }
295
296 if let Some(paren_pos) = next_line.find(')') {
298 let content_before_paren = &next_line[..paren_pos].trim();
300 if !content_before_paren.is_empty() {
301 array_lines.push((*content_before_paren).to_string());
302 }
303 break;
304 }
305
306 array_lines.push(next_line.to_string());
308 }
309
310 let array_content = array_lines
312 .iter()
313 .map(|s| s.trim())
314 .filter(|s| !s.is_empty())
315 .collect::<Vec<_>>()
316 .join(" ");
317 parse_array_content(&array_content)
318 },
319 |closing_paren_pos| {
320 let array_content = &value[1..closing_paren_pos];
322 parse_array_content(array_content)
323 },
324 );
325
326 let filtered_conflicts: Vec<String> = conflict_deps
328 .into_iter()
329 .filter_map(|conflict| {
330 let conflict_trimmed = conflict.trim();
331 if conflict_trimmed.is_empty() {
332 return None;
333 }
334
335 if is_valid_dependency(conflict_trimmed) {
336 let spec = parse_dep_spec(conflict_trimmed);
339 if !spec.name.is_empty() && seen.insert(spec.name.clone()) {
340 Some(spec.name)
341 } else {
342 None
343 }
344 } else {
345 None
346 }
347 })
348 .collect();
349
350 conflicts.extend(filtered_conflicts);
352 }
353 }
354 }
355
356 conflicts
357}
358
359fn find_matching_closing_paren(s: &str) -> Option<usize> {
370 let mut depth = 0;
371 let mut in_quotes = false;
372 let mut quote_char = '\0';
373
374 for (pos, ch) in s.char_indices() {
375 match ch {
376 '\'' | '"' => {
377 if !in_quotes {
378 in_quotes = true;
379 quote_char = ch;
380 } else if ch == quote_char {
381 in_quotes = false;
382 quote_char = '\0';
383 }
384 }
385 '(' if !in_quotes => {
386 depth += 1;
387 }
388 ')' if !in_quotes => {
389 depth -= 1;
390 if depth == 0 {
391 return Some(pos);
392 }
393 }
394 _ => {}
395 }
396 }
397 None
398}
399
400fn parse_array_content(content: &str) -> Vec<String> {
412 let mut deps = Vec::new();
413 let mut in_quotes = false;
414 let mut quote_char = '\0';
415 let mut current = String::new();
416
417 for ch in content.chars() {
418 match ch {
419 '\'' | '"' => {
420 if !in_quotes {
421 in_quotes = true;
422 quote_char = ch;
423 } else if ch == quote_char {
424 if !current.is_empty() {
425 deps.push(current.clone());
426 current.clear();
427 }
428 in_quotes = false;
429 quote_char = '\0';
430 } else {
431 current.push(ch);
432 }
433 }
434 _ if in_quotes => {
435 current.push(ch);
436 }
437 ch if ch.is_whitespace() => {
438 if !current.is_empty() {
440 deps.push(current.clone());
441 current.clear();
442 }
443 }
444 _ => {
445 current.push(ch);
447 }
448 }
449 }
450
451 if !current.is_empty() {
453 deps.push(current);
454 }
455
456 deps
457}
458
459fn is_valid_dependency(dep: &str) -> bool {
474 let dep_lower = dep.to_lowercase();
476 if std::path::Path::new(&dep_lower)
477 .extension()
478 .is_some_and(|ext| ext.eq_ignore_ascii_case("so"))
479 || dep_lower.contains(".so.")
480 || dep_lower.contains(".so=")
481 {
482 return false;
483 }
484
485 if dep.ends_with(')') {
489 if dep.contains(">=") || dep.contains("<=") || dep.contains("==") {
492 return false;
494 }
495 return false;
497 }
498
499 let Some(first_char) = dep.chars().next() else {
502 return false;
503 };
504 if !first_char.is_alphanumeric() && first_char != '_' {
505 return false;
506 }
507
508 if dep.len() < 2 {
510 return false;
511 }
512
513 let has_valid_chars = dep
516 .chars()
517 .any(|c| c.is_alphanumeric() || c == '-' || c == '_');
518 if !has_valid_chars {
519 return false;
520 }
521
522 true
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
532 fn test_parse_pkgbuild_deps_basic() {
533 let pkgbuild = r"
534pkgname=test-package
535pkgver=1.0.0
536depends=('foo' 'bar>=1.2')
537makedepends=('make' 'gcc')
538";
539
540 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
541
542 assert_eq!(depends.len(), 2);
543 assert!(depends.contains(&"foo".to_string()));
544 assert!(depends.contains(&"bar>=1.2".to_string()));
545
546 assert_eq!(makedepends.len(), 2);
547 assert!(makedepends.contains(&"make".to_string()));
548 assert!(makedepends.contains(&"gcc".to_string()));
549
550 assert_eq!(checkdepends.len(), 0);
551 assert_eq!(optdepends.len(), 0);
552 }
553
554 #[test]
555 fn test_parse_pkgbuild_deps_append() {
556 let pkgbuild = r#"
557pkgname=test-package
558pkgver=1.0.0
559package() {
560 depends+=(foo bar)
561 cd $_pkgname
562 make DESTDIR="$pkgdir" PREFIX=/usr install
563}
564"#;
565
566 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
567
568 assert_eq!(depends.len(), 2);
569 assert!(depends.contains(&"foo".to_string()));
570 assert!(depends.contains(&"bar".to_string()));
571
572 assert_eq!(makedepends.len(), 0);
573 assert_eq!(checkdepends.len(), 0);
574 assert_eq!(optdepends.len(), 0);
575 }
576
577 #[test]
578 fn test_parse_pkgbuild_deps_unquoted() {
579 let pkgbuild = r"
580pkgname=test-package
581depends=(foo bar libcairo.so libdbus-1.so)
582";
583
584 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
585
586 assert_eq!(depends.len(), 2);
588 assert!(depends.contains(&"foo".to_string()));
589 assert!(depends.contains(&"bar".to_string()));
590
591 assert_eq!(makedepends.len(), 0);
592 assert_eq!(checkdepends.len(), 0);
593 assert_eq!(optdepends.len(), 0);
594 }
595
596 #[test]
597 fn test_parse_pkgbuild_deps_multiline() {
598 let pkgbuild = r"
599pkgname=test-package
600depends=(
601 'foo'
602 'bar>=1.2'
603 'baz'
604)
605";
606
607 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
608
609 assert_eq!(depends.len(), 3);
610 assert!(depends.contains(&"foo".to_string()));
611 assert!(depends.contains(&"bar>=1.2".to_string()));
612 assert!(depends.contains(&"baz".to_string()));
613
614 assert_eq!(makedepends.len(), 0);
615 assert_eq!(checkdepends.len(), 0);
616 assert_eq!(optdepends.len(), 0);
617 }
618
619 #[test]
620 fn test_parse_pkgbuild_deps_makedepends_append() {
621 let pkgbuild = r"
622pkgname=test-package
623build() {
624 makedepends+=(cmake ninja)
625 cmake -B build
626}
627";
628
629 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
630
631 assert_eq!(makedepends.len(), 2);
632 assert!(makedepends.contains(&"cmake".to_string()));
633 assert!(makedepends.contains(&"ninja".to_string()));
634
635 assert_eq!(depends.len(), 0);
636 assert_eq!(checkdepends.len(), 0);
637 assert_eq!(optdepends.len(), 0);
638 }
639
640 #[test]
641 fn test_parse_pkgbuild_deps_jujutsu_git_scenario() {
642 let pkgbuild = r"
643pkgname=jujutsu-git
644pkgver=0.1.0
645pkgdesc=Git-compatible VCS that is both simple and powerful
646url=https://github.com/martinvonz/jj
647license=(Apache-2.0)
648arch=(i686 x86_64 armv6h armv7h)
649depends=(
650 glibc
651 libc.so
652 libm.so
653)
654makedepends=(
655 libgit2
656 libgit2.so
657 libssh2
658 libssh2.so)
659 openssh
660 git)
661cargo
662checkdepends=()
663optdepends=()
664source=($pkgname::git+$url)
665";
666
667 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
668
669 assert_eq!(depends.len(), 1);
671 assert!(depends.contains(&"glibc".to_string()));
672
673 assert_eq!(makedepends.len(), 2);
677 assert!(makedepends.contains(&"libgit2".to_string()));
678 assert!(makedepends.contains(&"libssh2".to_string()));
679
680 assert_eq!(checkdepends.len(), 0);
681 assert_eq!(optdepends.len(), 0);
682 }
683
684 #[test]
685 fn test_parse_pkgbuild_deps_ignore_other_fields() {
686 let pkgbuild = r"
687pkgname=test-package
688pkgver=1.0.0
689pkgdesc=Test package description
690url=https://example.com
691license=(MIT)
692arch=(x86_64)
693source=($pkgname-$pkgver.tar.gz)
694depends=(foo bar)
695makedepends=(make)
696";
697
698 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
699
700 assert_eq!(depends.len(), 2);
702 assert!(depends.contains(&"foo".to_string()));
703 assert!(depends.contains(&"bar".to_string()));
704
705 assert_eq!(makedepends.len(), 1);
706 assert!(makedepends.contains(&"make".to_string()));
707
708 assert_eq!(checkdepends.len(), 0);
709 assert_eq!(optdepends.len(), 0);
710 }
711
712 #[test]
713 fn test_parse_pkgbuild_deps_filter_invalid_names() {
714 let pkgbuild = r"
716depends=('valid-package' 'invalid)' '=invalid' 'a' 'valid>=1.0')
717";
718
719 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
720
721 assert_eq!(depends.len(), 2);
727 assert!(depends.contains(&"valid-package".to_string()));
728 assert!(depends.contains(&"valid>=1.0".to_string()));
729
730 assert_eq!(makedepends.len(), 0);
731 assert_eq!(checkdepends.len(), 0);
732 assert_eq!(optdepends.len(), 0);
733 }
734
735 #[test]
736 fn test_parse_pkgbuild_deps_deduplicates() {
737 let pkgbuild = r"
738depends=('foo' 'bar' 'foo' 'baz' 'bar')
739";
740
741 let (depends, _, _, _) = parse_pkgbuild_deps(pkgbuild);
742 assert_eq!(depends.len(), 3, "Should deduplicate dependencies");
743 assert!(depends.contains(&"foo".to_string()));
744 assert!(depends.contains(&"bar".to_string()));
745 assert!(depends.contains(&"baz".to_string()));
746 }
747
748 #[test]
749 fn test_parse_pkgbuild_deps_empty() {
750 let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps("");
751 assert_eq!(depends.len(), 0);
752 assert_eq!(makedepends.len(), 0);
753 assert_eq!(checkdepends.len(), 0);
754 assert_eq!(optdepends.len(), 0);
755 }
756
757 #[test]
758 fn test_parse_pkgbuild_deps_comments_and_blank_lines() {
759 let pkgbuild = r"
760# This is a comment
761pkgname=test-package
762
763depends=(foo bar)
764# Another comment
765makedepends=(make)
766";
767
768 let (depends, makedepends, _, _) = parse_pkgbuild_deps(pkgbuild);
769 assert_eq!(depends.len(), 2);
770 assert!(depends.contains(&"foo".to_string()));
771 assert!(depends.contains(&"bar".to_string()));
772 assert_eq!(makedepends.len(), 1);
773 assert!(makedepends.contains(&"make".to_string()));
774 }
775
776 #[test]
777 fn test_parse_pkgbuild_deps_mixed_quoted_unquoted() {
778 let pkgbuild = r"
779depends=('quoted' unquoted 'another-quoted' unquoted2)
780";
781
782 let (depends, _, _, _) = parse_pkgbuild_deps(pkgbuild);
783 assert_eq!(depends.len(), 4);
784 assert!(depends.contains(&"quoted".to_string()));
785 assert!(depends.contains(&"unquoted".to_string()));
786 assert!(depends.contains(&"another-quoted".to_string()));
787 assert!(depends.contains(&"unquoted2".to_string()));
788 }
789
790 #[test]
793 fn test_parse_pkgbuild_conflicts_basic() {
794 let pkgbuild = r"
795pkgname=jujutsu-git
796pkgver=0.1.0
797conflicts=('jujutsu')
798";
799
800 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
801
802 assert_eq!(conflicts.len(), 1);
803 assert!(conflicts.contains(&"jujutsu".to_string()));
804 }
805
806 #[test]
807 fn test_parse_pkgbuild_conflicts_multiline() {
808 let pkgbuild = r"
809pkgname=pacsea-git
810pkgver=0.1.0
811conflicts=(
812 'pacsea'
813 'pacsea-bin'
814)
815";
816
817 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
818
819 assert_eq!(conflicts.len(), 2);
820 assert!(conflicts.contains(&"pacsea".to_string()));
821 assert!(conflicts.contains(&"pacsea-bin".to_string()));
822 }
823
824 #[test]
825 fn test_parse_pkgbuild_conflicts_with_versions() {
826 let pkgbuild = r"
827pkgname=test-package
828conflicts=('old-pkg<2.0' 'new-pkg>=3.0')
829";
830
831 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
832
833 assert_eq!(conflicts.len(), 2);
834 assert!(conflicts.contains(&"old-pkg".to_string()));
835 assert!(conflicts.contains(&"new-pkg".to_string()));
836 }
837
838 #[test]
839 fn test_parse_pkgbuild_conflicts_filter_so() {
840 let pkgbuild = r"
841pkgname=test-package
842conflicts=('foo' 'libcairo.so' 'bar' 'libdbus-1.so=1-64')
843";
844
845 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
846
847 assert_eq!(conflicts.len(), 2);
849 assert!(conflicts.contains(&"foo".to_string()));
850 assert!(conflicts.contains(&"bar".to_string()));
851 }
852
853 #[test]
854 fn test_parse_pkgbuild_conflicts_deduplicates() {
855 let pkgbuild = r"
856conflicts=('pkg1' 'pkg2' 'pkg1' 'pkg3')
857";
858
859 let conflicts = parse_pkgbuild_conflicts(pkgbuild);
860 assert_eq!(conflicts.len(), 3, "Should deduplicate conflicts");
861 assert!(conflicts.contains(&"pkg1".to_string()));
862 assert!(conflicts.contains(&"pkg2".to_string()));
863 assert!(conflicts.contains(&"pkg3".to_string()));
864 }
865
866 #[test]
867 fn test_parse_pkgbuild_conflicts_empty() {
868 let conflicts = parse_pkgbuild_conflicts("");
869 assert!(conflicts.is_empty());
870 }
871
872 #[test]
873 fn test_multiline_array_keeps_first_line_entry() {
886 let pkgbuild = "optdepends=('cups: printing support'\n 'foo: extra feature')\ndepends=('glibc'\n 'gcc-libs'\n)";
887 let (depends, _makedepends, _checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
888 assert_eq!(
889 optdepends,
890 vec![
891 "cups: printing support".to_string(),
892 "foo: extra feature".to_string()
893 ]
894 );
895 assert_eq!(depends, vec!["glibc".to_string(), "gcc-libs".to_string()]);
896 }
897}