use std::collections::HashSet;
use std::sync::OnceLock;
fn get_depends_labels() -> &'static HashSet<String> {
static LABELS: OnceLock<HashSet<String>> = OnceLock::new();
LABELS.get_or_init(|| {
let mut labels = HashSet::new();
let locales_dir = crate::i18n::find_locales_dir().unwrap_or_else(|| {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("config")
.join("locales")
});
if let Ok(entries) = std::fs::read_dir(&locales_dir) {
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str()
&& file_name.to_lowercase().ends_with(".yml")
{
let locale = file_name.strip_suffix(".yml").unwrap_or(file_name);
if let Ok(translations) = crate::i18n::load_locale_file(locale, &locales_dir) {
if let Some(labels_str) =
translations.get("app.parsing.pacman_depends_labels")
&& let Ok(yaml_value) =
serde_norway::from_str::<serde_norway::Value>(labels_str)
&& let Some(seq) = yaml_value.as_sequence()
{
for item in seq {
if let Some(label) = item.as_str() {
labels.insert(label.to_string());
}
}
} else if let Some(label) =
translations.get("app.parsing.pacman_depends_label")
{
labels.insert(label.clone());
}
}
}
}
}
labels.insert("Depends On".to_string());
labels.insert("Ist abhängig von".to_string());
labels.insert("Dépend de".to_string());
labels.insert("Depende de".to_string());
labels.insert("Dipende da".to_string());
labels.insert("Zależy od".to_string());
labels.insert("Зависит от".to_string());
labels.insert("依存".to_string());
labels
})
}
fn get_none_labels() -> &'static HashSet<String> {
static LABELS: OnceLock<HashSet<String>> = OnceLock::new();
LABELS.get_or_init(|| {
let mut labels = HashSet::new();
let locales_dir = crate::i18n::find_locales_dir().unwrap_or_else(|| {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("config")
.join("locales")
});
if let Ok(entries) = std::fs::read_dir(&locales_dir) {
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str()
&& file_name.to_lowercase().ends_with(".yml")
{
let locale = file_name.strip_suffix(".yml").unwrap_or(file_name);
if let Ok(translations) = crate::i18n::load_locale_file(locale, &locales_dir) {
if let Some(labels_str) = translations.get("app.parsing.pacman_none_labels")
&& let Ok(yaml_value) =
serde_norway::from_str::<serde_norway::Value>(labels_str)
&& let Some(seq) = yaml_value.as_sequence()
{
for item in seq {
if let Some(label) = item.as_str() {
labels.insert(label.to_string());
}
}
} else if let Some(label) =
translations.get("app.parsing.pacman_none_label")
{
labels.insert(label.clone());
}
}
}
}
}
labels.insert("None".to_string());
labels.insert("Keine".to_string());
labels.insert("Aucune".to_string());
labels.insert("Ninguna".to_string());
labels.insert("Nessuna".to_string());
labels
})
}
pub(super) fn parse_pacman_si_deps(text: &str) -> Vec<String> {
let depends_labels = get_depends_labels();
let none_labels = get_none_labels();
for line in text.lines() {
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 deps_str = line[colon_pos + 1..].trim();
if deps_str.is_empty()
|| none_labels
.iter()
.any(|label| deps_str.eq_ignore_ascii_case(label))
{
return Vec::new();
}
return deps_str
.split_whitespace()
.map(|s| s.trim().to_string())
.filter(|s| {
#[allow(clippy::case_sensitive_file_extension_comparisons)]
{
if s.is_empty() {
return false;
}
let s_lower = s.to_lowercase();
if s_lower.ends_with(".so")
|| s_lower.contains(".so.")
|| s_lower.contains(".so=")
{
return false;
}
}
let common_words = [
"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",
];
let lower = s.to_lowercase();
if common_words.contains(&lower.as_str()) {
return false;
}
if s.len() < 2 {
return false;
}
let first_char = s.chars().next().unwrap_or(' ');
if !first_char.is_alphanumeric() && first_char != '-' && first_char != '_' {
return false;
}
if s.ends_with(':') {
return false;
}
if !s.chars().any(char::is_alphanumeric) {
return false;
}
true
})
.collect();
}
}
Vec::new()
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
pub(super) fn parse_pacman_si_conflicts(text: &str) -> Vec<String> {
let none_labels = get_none_labels();
for line in text.lines() {
let is_conflicts_line = line.starts_with("Conflicts With")
|| line.starts_with("Konflikt mit")
|| (line.contains("Conflicts") && line.contains("With"));
if is_conflicts_line && let Some(colon_pos) = line.find(':') {
let conflicts_str = line[colon_pos + 1..].trim();
if conflicts_str.is_empty()
|| none_labels
.iter()
.any(|label| conflicts_str.eq_ignore_ascii_case(label))
{
return Vec::new();
}
return conflicts_str
.split_whitespace()
.map(|s| s.trim().to_string())
.filter(|s| {
if s.is_empty() {
return false;
}
let s_lower = s.to_lowercase();
if s_lower.ends_with(".so")
|| s_lower.contains(".so.")
|| s_lower.contains(".so=")
{
return false;
}
let common_words = [
"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",
];
let lower = s.to_lowercase();
if common_words.contains(&lower.as_str()) {
return false;
}
if s.len() < 2 {
return false;
}
let first_char = s.chars().next().unwrap_or(' ');
if !first_char.is_alphanumeric() && first_char != '-' && first_char != '_' {
return false;
}
if s.ends_with(':') {
return false;
}
if !s.chars().any(char::is_alphanumeric) {
return false;
}
true
})
.map(|s| {
parse_dep_spec(&s).0
})
.collect();
}
}
Vec::new()
}
pub(super) fn parse_dep_spec(spec: &str) -> (String, String) {
for op in ["<=", ">=", "=", "<", ">"] {
if let Some(pos) = spec.find(op) {
let name = spec[..pos].trim().to_string();
let version = spec[pos..].trim().to_string();
return (name, version);
}
}
(spec.trim().to_string(), String::new())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_dep_spec_basic() {
let (name, version) = parse_dep_spec("glibc");
assert_eq!(name, "glibc");
assert_eq!(version, "");
}
#[test]
fn parse_dep_spec_with_version() {
let (name, version) = parse_dep_spec("python>=3.12");
assert_eq!(name, "python");
assert_eq!(version, ">=3.12");
}
#[test]
fn parse_dep_spec_equals() {
let (name, version) = parse_dep_spec("firefox=121.0");
assert_eq!(name, "firefox");
assert_eq!(version, "=121.0");
}
#[test]
fn parse_pacman_si_conflicts_basic() {
let text =
"Name : test-package\nConflicts 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 = "Name : test-package\nConflicts 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 = "Name : test-package\nConflicts With : None\n";
let conflicts = parse_pacman_si_conflicts(text);
assert!(conflicts.is_empty());
}
}