use std::collections::HashSet;
use crate::deps::parse::parse_dep_spec;
#[allow(clippy::case_sensitive_file_extension_comparisons)]
#[must_use]
pub fn parse_pkgbuild_deps(pkgbuild: &str) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
let mut depends = Vec::new();
let mut makedepends = Vec::new();
let mut checkdepends = Vec::new();
let mut optdepends = Vec::new();
let mut seen_depends = HashSet::new();
let mut seen_makedepends = HashSet::new();
let mut seen_checkdepends = HashSet::new();
let mut seen_optdepends = HashSet::new();
let lines: Vec<&str> = pkgbuild.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim();
i += 1;
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim();
let base_key = key.strip_suffix('+').map_or(key, |stripped| stripped);
if !matches!(
base_key,
"depends" | "makedepends" | "checkdepends" | "optdepends"
) {
continue;
}
if value.starts_with('(') {
let deps = find_matching_closing_paren(value).map_or_else(
|| {
let mut array_lines = Vec::new();
while i < lines.len() {
let next_line = lines[i].trim();
i += 1;
if next_line.is_empty() || next_line.starts_with('#') {
continue;
}
if next_line == ")" {
break;
}
if let Some(paren_pos) = next_line.find(')') {
let content_before_paren = &next_line[..paren_pos].trim();
if !content_before_paren.is_empty() {
array_lines.push((*content_before_paren).to_string());
}
break;
}
array_lines.push(next_line.to_string());
}
let array_content = array_lines
.iter()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
parse_array_content(&array_content)
},
|closing_paren_pos| {
let array_content = &value[1..closing_paren_pos];
parse_array_content(array_content)
},
);
let filtered_deps: Vec<String> = deps
.into_iter()
.filter_map(|dep| {
let dep_trimmed = dep.trim();
if dep_trimmed.is_empty() {
return None;
}
if is_valid_dependency(dep_trimmed) {
Some(dep_trimmed.to_string())
} else {
None
}
})
.collect();
match base_key {
"depends" => {
for dep in filtered_deps {
if seen_depends.insert(dep.clone()) {
depends.push(dep);
}
}
}
"makedepends" => {
for dep in filtered_deps {
if seen_makedepends.insert(dep.clone()) {
makedepends.push(dep);
}
}
}
"checkdepends" => {
for dep in filtered_deps {
if seen_checkdepends.insert(dep.clone()) {
checkdepends.push(dep);
}
}
}
"optdepends" => {
for dep in filtered_deps {
if seen_optdepends.insert(dep.clone()) {
optdepends.push(dep);
}
}
}
_ => {}
}
}
}
}
(depends, makedepends, checkdepends, optdepends)
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
#[must_use]
pub fn parse_pkgbuild_conflicts(pkgbuild: &str) -> Vec<String> {
let mut conflicts = Vec::new();
let mut seen = HashSet::new();
let lines: Vec<&str> = pkgbuild.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim();
i += 1;
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim();
let base_key = key.strip_suffix('+').map_or(key, |stripped| stripped);
if base_key != "conflicts" {
continue;
}
if value.starts_with('(') {
let conflict_deps = find_matching_closing_paren(value).map_or_else(
|| {
let mut array_lines = Vec::new();
while i < lines.len() {
let next_line = lines[i].trim();
i += 1;
if next_line.is_empty() || next_line.starts_with('#') {
continue;
}
if next_line == ")" {
break;
}
if let Some(paren_pos) = next_line.find(')') {
let content_before_paren = &next_line[..paren_pos].trim();
if !content_before_paren.is_empty() {
array_lines.push((*content_before_paren).to_string());
}
break;
}
array_lines.push(next_line.to_string());
}
let array_content = array_lines
.iter()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
parse_array_content(&array_content)
},
|closing_paren_pos| {
let array_content = &value[1..closing_paren_pos];
parse_array_content(array_content)
},
);
let filtered_conflicts: Vec<String> = conflict_deps
.into_iter()
.filter_map(|conflict| {
let conflict_trimmed = conflict.trim();
if conflict_trimmed.is_empty() {
return None;
}
if is_valid_dependency(conflict_trimmed) {
let spec = parse_dep_spec(conflict_trimmed);
if !spec.name.is_empty() && seen.insert(spec.name.clone()) {
Some(spec.name)
} else {
None
}
} else {
None
}
})
.collect();
conflicts.extend(filtered_conflicts);
}
}
}
conflicts
}
fn find_matching_closing_paren(s: &str) -> Option<usize> {
let mut depth = 0;
let mut in_quotes = false;
let mut quote_char = '\0';
for (pos, ch) in s.char_indices() {
match ch {
'\'' | '"' => {
if !in_quotes {
in_quotes = true;
quote_char = ch;
} else if ch == quote_char {
in_quotes = false;
quote_char = '\0';
}
}
'(' if !in_quotes => {
depth += 1;
}
')' if !in_quotes => {
depth -= 1;
if depth == 0 {
return Some(pos);
}
}
_ => {}
}
}
None
}
fn parse_array_content(content: &str) -> Vec<String> {
let mut deps = Vec::new();
let mut in_quotes = false;
let mut quote_char = '\0';
let mut current = String::new();
for ch in content.chars() {
match ch {
'\'' | '"' => {
if !in_quotes {
in_quotes = true;
quote_char = ch;
} else if ch == quote_char {
if !current.is_empty() {
deps.push(current.clone());
current.clear();
}
in_quotes = false;
quote_char = '\0';
} else {
current.push(ch);
}
}
_ if in_quotes => {
current.push(ch);
}
ch if ch.is_whitespace() => {
if !current.is_empty() {
deps.push(current.clone());
current.clear();
}
}
_ => {
current.push(ch);
}
}
}
if !current.is_empty() {
deps.push(current);
}
deps
}
fn is_valid_dependency(dep: &str) -> bool {
let dep_lower = dep.to_lowercase();
if std::path::Path::new(&dep_lower)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("so"))
|| dep_lower.contains(".so.")
|| dep_lower.contains(".so=")
{
return false;
}
if dep.ends_with(')') {
if dep.contains(">=") || dep.contains("<=") || dep.contains("==") {
return false;
}
return false;
}
let Some(first_char) = dep.chars().next() else {
return false;
};
if !first_char.is_alphanumeric() && first_char != '_' {
return false;
}
if dep.len() < 2 {
return false;
}
let has_valid_chars = dep
.chars()
.any(|c| c.is_alphanumeric() || c == '-' || c == '_');
if !has_valid_chars {
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_pkgbuild_deps_basic() {
let pkgbuild = r"
pkgname=test-package
pkgver=1.0.0
depends=('foo' 'bar>=1.2')
makedepends=('make' 'gcc')
";
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 2);
assert!(depends.contains(&"foo".to_string()));
assert!(depends.contains(&"bar>=1.2".to_string()));
assert_eq!(makedepends.len(), 2);
assert!(makedepends.contains(&"make".to_string()));
assert!(makedepends.contains(&"gcc".to_string()));
assert_eq!(checkdepends.len(), 0);
assert_eq!(optdepends.len(), 0);
}
#[test]
fn test_parse_pkgbuild_deps_append() {
let pkgbuild = r#"
pkgname=test-package
pkgver=1.0.0
package() {
depends+=(foo bar)
cd $_pkgname
make DESTDIR="$pkgdir" PREFIX=/usr install
}
"#;
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 2);
assert!(depends.contains(&"foo".to_string()));
assert!(depends.contains(&"bar".to_string()));
assert_eq!(makedepends.len(), 0);
assert_eq!(checkdepends.len(), 0);
assert_eq!(optdepends.len(), 0);
}
#[test]
fn test_parse_pkgbuild_deps_unquoted() {
let pkgbuild = r"
pkgname=test-package
depends=(foo bar libcairo.so libdbus-1.so)
";
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 2);
assert!(depends.contains(&"foo".to_string()));
assert!(depends.contains(&"bar".to_string()));
assert_eq!(makedepends.len(), 0);
assert_eq!(checkdepends.len(), 0);
assert_eq!(optdepends.len(), 0);
}
#[test]
fn test_parse_pkgbuild_deps_multiline() {
let pkgbuild = r"
pkgname=test-package
depends=(
'foo'
'bar>=1.2'
'baz'
)
";
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 3);
assert!(depends.contains(&"foo".to_string()));
assert!(depends.contains(&"bar>=1.2".to_string()));
assert!(depends.contains(&"baz".to_string()));
assert_eq!(makedepends.len(), 0);
assert_eq!(checkdepends.len(), 0);
assert_eq!(optdepends.len(), 0);
}
#[test]
fn test_parse_pkgbuild_deps_makedepends_append() {
let pkgbuild = r"
pkgname=test-package
build() {
makedepends+=(cmake ninja)
cmake -B build
}
";
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(makedepends.len(), 2);
assert!(makedepends.contains(&"cmake".to_string()));
assert!(makedepends.contains(&"ninja".to_string()));
assert_eq!(depends.len(), 0);
assert_eq!(checkdepends.len(), 0);
assert_eq!(optdepends.len(), 0);
}
#[test]
fn test_parse_pkgbuild_deps_jujutsu_git_scenario() {
let pkgbuild = r"
pkgname=jujutsu-git
pkgver=0.1.0
pkgdesc=Git-compatible VCS that is both simple and powerful
url=https://github.com/martinvonz/jj
license=(Apache-2.0)
arch=(i686 x86_64 armv6h armv7h)
depends=(
glibc
libc.so
libm.so
)
makedepends=(
libgit2
libgit2.so
libssh2
libssh2.so)
openssh
git)
cargo
checkdepends=()
optdepends=()
source=($pkgname::git+$url)
";
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 1);
assert!(depends.contains(&"glibc".to_string()));
assert_eq!(makedepends.len(), 2);
assert!(makedepends.contains(&"libgit2".to_string()));
assert!(makedepends.contains(&"libssh2".to_string()));
assert_eq!(checkdepends.len(), 0);
assert_eq!(optdepends.len(), 0);
}
#[test]
fn test_parse_pkgbuild_deps_ignore_other_fields() {
let pkgbuild = r"
pkgname=test-package
pkgver=1.0.0
pkgdesc=Test package description
url=https://example.com
license=(MIT)
arch=(x86_64)
source=($pkgname-$pkgver.tar.gz)
depends=(foo bar)
makedepends=(make)
";
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 2);
assert!(depends.contains(&"foo".to_string()));
assert!(depends.contains(&"bar".to_string()));
assert_eq!(makedepends.len(), 1);
assert!(makedepends.contains(&"make".to_string()));
assert_eq!(checkdepends.len(), 0);
assert_eq!(optdepends.len(), 0);
}
#[test]
fn test_parse_pkgbuild_deps_filter_invalid_names() {
let pkgbuild = r"
depends=('valid-package' 'invalid)' '=invalid' 'a' 'valid>=1.0')
";
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 2);
assert!(depends.contains(&"valid-package".to_string()));
assert!(depends.contains(&"valid>=1.0".to_string()));
assert_eq!(makedepends.len(), 0);
assert_eq!(checkdepends.len(), 0);
assert_eq!(optdepends.len(), 0);
}
#[test]
fn test_parse_pkgbuild_deps_deduplicates() {
let pkgbuild = r"
depends=('foo' 'bar' 'foo' 'baz' 'bar')
";
let (depends, _, _, _) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 3, "Should deduplicate dependencies");
assert!(depends.contains(&"foo".to_string()));
assert!(depends.contains(&"bar".to_string()));
assert!(depends.contains(&"baz".to_string()));
}
#[test]
fn test_parse_pkgbuild_deps_empty() {
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps("");
assert_eq!(depends.len(), 0);
assert_eq!(makedepends.len(), 0);
assert_eq!(checkdepends.len(), 0);
assert_eq!(optdepends.len(), 0);
}
#[test]
fn test_parse_pkgbuild_deps_comments_and_blank_lines() {
let pkgbuild = r"
# This is a comment
pkgname=test-package
depends=(foo bar)
# Another comment
makedepends=(make)
";
let (depends, makedepends, _, _) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 2);
assert!(depends.contains(&"foo".to_string()));
assert!(depends.contains(&"bar".to_string()));
assert_eq!(makedepends.len(), 1);
assert!(makedepends.contains(&"make".to_string()));
}
#[test]
fn test_parse_pkgbuild_deps_mixed_quoted_unquoted() {
let pkgbuild = r"
depends=('quoted' unquoted 'another-quoted' unquoted2)
";
let (depends, _, _, _) = parse_pkgbuild_deps(pkgbuild);
assert_eq!(depends.len(), 4);
assert!(depends.contains(&"quoted".to_string()));
assert!(depends.contains(&"unquoted".to_string()));
assert!(depends.contains(&"another-quoted".to_string()));
assert!(depends.contains(&"unquoted2".to_string()));
}
#[test]
fn test_parse_pkgbuild_conflicts_basic() {
let pkgbuild = r"
pkgname=jujutsu-git
pkgver=0.1.0
conflicts=('jujutsu')
";
let conflicts = parse_pkgbuild_conflicts(pkgbuild);
assert_eq!(conflicts.len(), 1);
assert!(conflicts.contains(&"jujutsu".to_string()));
}
#[test]
fn test_parse_pkgbuild_conflicts_multiline() {
let pkgbuild = r"
pkgname=pacsea-git
pkgver=0.1.0
conflicts=(
'pacsea'
'pacsea-bin'
)
";
let conflicts = parse_pkgbuild_conflicts(pkgbuild);
assert_eq!(conflicts.len(), 2);
assert!(conflicts.contains(&"pacsea".to_string()));
assert!(conflicts.contains(&"pacsea-bin".to_string()));
}
#[test]
fn test_parse_pkgbuild_conflicts_with_versions() {
let pkgbuild = r"
pkgname=test-package
conflicts=('old-pkg<2.0' 'new-pkg>=3.0')
";
let conflicts = parse_pkgbuild_conflicts(pkgbuild);
assert_eq!(conflicts.len(), 2);
assert!(conflicts.contains(&"old-pkg".to_string()));
assert!(conflicts.contains(&"new-pkg".to_string()));
}
#[test]
fn test_parse_pkgbuild_conflicts_filter_so() {
let pkgbuild = r"
pkgname=test-package
conflicts=('foo' 'libcairo.so' 'bar' 'libdbus-1.so=1-64')
";
let conflicts = parse_pkgbuild_conflicts(pkgbuild);
assert_eq!(conflicts.len(), 2);
assert!(conflicts.contains(&"foo".to_string()));
assert!(conflicts.contains(&"bar".to_string()));
}
#[test]
fn test_parse_pkgbuild_conflicts_deduplicates() {
let pkgbuild = r"
conflicts=('pkg1' 'pkg2' 'pkg1' 'pkg3')
";
let conflicts = parse_pkgbuild_conflicts(pkgbuild);
assert_eq!(conflicts.len(), 3, "Should deduplicate conflicts");
assert!(conflicts.contains(&"pkg1".to_string()));
assert!(conflicts.contains(&"pkg2".to_string()));
assert!(conflicts.contains(&"pkg3".to_string()));
}
#[test]
fn test_parse_pkgbuild_conflicts_empty() {
let conflicts = parse_pkgbuild_conflicts("");
assert!(conflicts.is_empty());
}
}