use std::collections::HashSet;
use crate::types::DependencySpec;
const DEPENDS_LABELS: &[&str] = &["Depends On"];
const NONE_LABELS: &[&str] = &["None"];
const CONFLICTS_LABELS: &[&str] = &["Conflicts With"];
const COMMON_WORDS: &[&str] = &[
"for", "to", "with", "is", "that", "using", "usually", "bundled", "bindings", "tooling", "the",
"and", "or", "in", "on", "at", "by", "from", "as", "if", "when", "where", "which", "what",
"how", "why",
];
#[must_use]
pub fn parse_dep_spec(spec: &str) -> DependencySpec {
for op in ["<=", ">=", "=", "<", ">"] {
if let Some(pos) = spec.find(op) {
return DependencySpec {
name: spec[..pos].trim().to_string(),
version_req: spec[pos..].trim().to_string(),
};
}
}
DependencySpec::new(spec.trim())
}
fn is_valid_package_token(token: &str) -> bool {
if token.is_empty() || token.len() < 2 {
return false;
}
let lower = token.to_lowercase();
#[allow(clippy::case_sensitive_file_extension_comparisons)]
{
if lower.ends_with(".so") || lower.contains(".so.") || lower.contains(".so=") {
return false;
}
}
if COMMON_WORDS.contains(&lower.as_str()) {
return false;
}
let Some(first_char) = token.chars().next() else {
return false;
};
if !first_char.is_alphanumeric() && first_char != '-' && first_char != '_' {
return false;
}
if token.ends_with(':') {
return false;
}
token.chars().any(char::is_alphanumeric)
}
fn collect_continuation_lines(lines: &[&str], start_index: usize, field_value: &str) -> String {
let mut result = field_value.to_string();
for continuation_line in lines.iter().skip(start_index + 1) {
if continuation_line.trim().is_empty() {
break;
}
let trimmed = continuation_line.trim_start();
if continuation_line.starts_with(char::is_whitespace) {
result.push(' ');
result.push_str(trimmed);
} else if trimmed.contains(':') && !trimmed.starts_with(char::is_whitespace) {
break;
} else {
break;
}
}
result
}
#[must_use]
pub fn parse_pacman_si_deps(text: &str) -> Vec<String> {
let lines: Vec<&str> = text.lines().collect();
for (i, line) in lines.iter().enumerate() {
let is_depends_line = DEPENDS_LABELS.iter().any(|label| line.starts_with(label))
|| (line.contains("Depends") && line.contains("On"));
if is_depends_line && let Some(colon_pos) = line.find(':') {
let initial_value = line[colon_pos + 1..].trim();
let deps_str = collect_continuation_lines(&lines, i, initial_value);
let deps_str_trimmed = deps_str.trim();
if deps_str_trimmed.is_empty()
|| NONE_LABELS
.iter()
.any(|label| deps_str_trimmed.eq_ignore_ascii_case(label))
{
return Vec::new();
}
let mut seen = HashSet::new();
return deps_str_trimmed
.split_whitespace()
.map(str::trim)
.filter(|s| is_valid_package_token(s))
.filter_map(|s| {
if seen.insert(s) {
Some(s.to_string())
} else {
None
}
})
.collect();
}
}
Vec::new()
}
#[must_use]
pub fn parse_pacman_si_conflicts(text: &str) -> Vec<String> {
let lines: Vec<&str> = text.lines().collect();
for (i, line) in lines.iter().enumerate() {
let is_conflicts_line = CONFLICTS_LABELS.iter().any(|label| line.starts_with(label))
|| (line.contains("Conflicts") && line.contains("With"));
if is_conflicts_line && let Some(colon_pos) = line.find(':') {
let initial_value = line[colon_pos + 1..].trim();
let conflicts_str = collect_continuation_lines(&lines, i, initial_value);
let conflicts_str_trimmed = conflicts_str.trim();
if conflicts_str_trimmed.is_empty()
|| NONE_LABELS
.iter()
.any(|label| conflicts_str_trimmed.eq_ignore_ascii_case(label))
{
return Vec::new();
}
let mut seen = HashSet::new();
return conflicts_str_trimmed
.split_whitespace()
.map(str::trim)
.filter(|s| is_valid_package_token(s))
.map(|s| parse_dep_spec(s).name)
.filter_map(|name| {
if seen.insert(name.clone()) {
Some(name)
} else {
None
}
})
.collect();
}
}
Vec::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_dep_spec_no_version() {
let spec = parse_dep_spec("glibc");
assert_eq!(spec.name, "glibc");
assert!(spec.version_req.is_empty());
assert!(!spec.has_version_req());
}
#[test]
fn parse_dep_spec_greater_equal() {
let spec = parse_dep_spec("python>=3.12");
assert_eq!(spec.name, "python");
assert_eq!(spec.version_req, ">=3.12");
}
#[test]
fn parse_dep_spec_less_equal() {
let spec = parse_dep_spec("openssl<=1.1.1");
assert_eq!(spec.name, "openssl");
assert_eq!(spec.version_req, "<=1.1.1");
}
#[test]
fn parse_dep_spec_equal() {
let spec = parse_dep_spec("firefox=121.0");
assert_eq!(spec.name, "firefox");
assert_eq!(spec.version_req, "=121.0");
}
#[test]
fn parse_dep_spec_greater() {
let spec = parse_dep_spec("rust>1.70");
assert_eq!(spec.name, "rust");
assert_eq!(spec.version_req, ">1.70");
}
#[test]
fn parse_dep_spec_less() {
let spec = parse_dep_spec("cmake<4.0");
assert_eq!(spec.name, "cmake");
assert_eq!(spec.version_req, "<4.0");
}
#[test]
fn parse_dep_spec_with_whitespace() {
let spec = parse_dep_spec(" python >= 3.12 ");
assert_eq!(spec.name, "python");
assert_eq!(spec.version_req, ">= 3.12");
}
#[test]
fn parse_dep_spec_complex_version() {
let spec = parse_dep_spec("qt5-base>=5.15.10-1");
assert_eq!(spec.name, "qt5-base");
assert_eq!(spec.version_req, ">=5.15.10-1");
}
#[test]
fn is_valid_package_token_valid() {
assert!(is_valid_package_token("glibc"));
assert!(is_valid_package_token("qt5-base"));
assert!(is_valid_package_token("python3"));
assert!(is_valid_package_token("lib32-glibc"));
}
#[test]
fn is_valid_package_token_so_files() {
assert!(!is_valid_package_token("libedit.so"));
assert!(!is_valid_package_token("libgit2.so.1"));
assert!(!is_valid_package_token("libfoo.so=0-64"));
}
#[test]
fn is_valid_package_token_common_words() {
assert!(!is_valid_package_token("for"));
assert!(!is_valid_package_token("with"));
assert!(!is_valid_package_token("the"));
}
#[test]
fn is_valid_package_token_short() {
assert!(!is_valid_package_token("a"));
assert!(!is_valid_package_token(""));
}
#[test]
fn is_valid_package_token_invalid_start() {
assert!(!is_valid_package_token("(test)"));
assert!(!is_valid_package_token("[optional]"));
}
#[test]
fn is_valid_package_token_colon_ending() {
assert!(!is_valid_package_token("error:"));
}
#[test]
fn parse_pacman_si_deps_basic() {
let text = "Name : firefox\nDepends On : glibc gtk3 nss\n";
let deps = parse_pacman_si_deps(text);
assert_eq!(deps.len(), 3);
assert!(deps.contains(&"glibc".to_string()));
assert!(deps.contains(&"gtk3".to_string()));
assert!(deps.contains(&"nss".to_string()));
}
#[test]
fn parse_pacman_si_deps_with_versions() {
let text = "Depends On : python>=3.10 rust>=1.70\n";
let deps = parse_pacman_si_deps(text);
assert_eq!(deps.len(), 2);
assert!(deps.contains(&"python>=3.10".to_string()));
assert!(deps.contains(&"rust>=1.70".to_string()));
}
#[test]
fn parse_pacman_si_deps_none() {
let text = "Depends On : None\n";
let deps = parse_pacman_si_deps(text);
assert!(deps.is_empty());
}
#[test]
fn parse_pacman_si_deps_empty() {
let text = "Depends On :\n";
let deps = parse_pacman_si_deps(text);
assert!(deps.is_empty());
}
#[test]
fn parse_pacman_si_deps_filters_so() {
let text = "Depends On : glibc libedit.so libgit2.so.1 nss\n";
let deps = parse_pacman_si_deps(text);
assert_eq!(deps.len(), 2);
assert!(deps.contains(&"glibc".to_string()));
assert!(deps.contains(&"nss".to_string()));
}
#[test]
fn parse_pacman_si_deps_no_depends_line() {
let text = "Name : firefox\nVersion : 121.0\n";
let deps = parse_pacman_si_deps(text);
assert!(deps.is_empty());
}
#[test]
fn parse_pacman_si_deps_deduplicates() {
let text = "Depends On : glibc gtk3 glibc nss gtk3\n";
let deps = parse_pacman_si_deps(text);
assert_eq!(deps.len(), 3, "Should deduplicate dependencies");
assert!(deps.contains(&"glibc".to_string()));
assert!(deps.contains(&"gtk3".to_string()));
assert!(deps.contains(&"nss".to_string()));
}
#[test]
fn parse_pacman_si_deps_multiline() {
let text = "Name : firefox\nDepends On : glibc gtk3 libpulse nss\n libxt libxss libxcomposite\n libx11 libxcb\n";
let deps = parse_pacman_si_deps(text);
assert_eq!(deps.len(), 9);
assert!(deps.contains(&"glibc".to_string()));
assert!(deps.contains(&"gtk3".to_string()));
assert!(deps.contains(&"libpulse".to_string()));
assert!(deps.contains(&"nss".to_string()));
assert!(deps.contains(&"libxt".to_string()));
assert!(deps.contains(&"libxss".to_string()));
assert!(deps.contains(&"libxcomposite".to_string()));
assert!(deps.contains(&"libx11".to_string()));
assert!(deps.contains(&"libxcb".to_string()));
}
#[test]
fn parse_pacman_si_conflicts_basic() {
let text = "Conflicts With : conflicting-pkg1 conflicting-pkg2\n";
let conflicts = parse_pacman_si_conflicts(text);
assert_eq!(conflicts.len(), 2);
assert!(conflicts.contains(&"conflicting-pkg1".to_string()));
assert!(conflicts.contains(&"conflicting-pkg2".to_string()));
}
#[test]
fn parse_pacman_si_conflicts_with_versions() {
let text = "Conflicts With : old-pkg<2.0 new-pkg>=3.0\n";
let conflicts = parse_pacman_si_conflicts(text);
assert_eq!(conflicts.len(), 2);
assert!(conflicts.contains(&"old-pkg".to_string()));
assert!(conflicts.contains(&"new-pkg".to_string()));
}
#[test]
fn parse_pacman_si_conflicts_none() {
let text = "Conflicts With : None\n";
let conflicts = parse_pacman_si_conflicts(text);
assert!(conflicts.is_empty());
}
#[test]
fn parse_pacman_si_conflicts_empty() {
let text = "Conflicts With :\n";
let conflicts = parse_pacman_si_conflicts(text);
assert!(conflicts.is_empty());
}
#[test]
fn parse_pacman_si_conflicts_no_conflicts_line() {
let text = "Name : firefox\nVersion : 121.0\n";
let conflicts = parse_pacman_si_conflicts(text);
assert!(conflicts.is_empty());
}
#[test]
fn parse_pacman_si_conflicts_deduplicates() {
let text = "Conflicts With : pkg1 pkg2 pkg1 pkg3\n";
let conflicts = parse_pacman_si_conflicts(text);
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()));
}
}