use std::collections::HashMap;
use crate::PackageFormat;
fn parse_provides_apk_pacman(provides_str: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
for part in provides_str.split_whitespace() {
if part.is_empty() {
continue;
}
if let Some(equals_pos) = part.find('=') {
let name = part[..equals_pos].to_string();
let version = part[equals_pos + 1..].to_string();
if version.contains(".so") && !version.chars().next().map_or(false, |c| c.is_ascii_digit()) {
result.insert(name, String::new());
result.insert(version, String::new());
} else {
result.insert(name, version);
}
} else {
result.insert(part.to_string(), String::new());
}
}
result
}
fn parse_provides_deb(provides_str: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
for item in provides_str.split(',') {
let trimmed = item.trim();
if trimmed.is_empty() {
continue;
}
let mut provide_name = trimmed.to_string();
let mut version = String::new();
if let Some(paren_start) = trimmed.find('(') {
if trimmed[paren_start..].starts_with("(= ")
|| trimmed[paren_start..].starts_with("(>= ")
|| trimmed[paren_start..].starts_with("(<= ")
|| trimmed[paren_start..].starts_with("(> ")
|| trimmed[paren_start..].starts_with("(< ")
{
provide_name = trimmed[..paren_start].trim_end().to_string();
if let Some(paren_end) = trimmed[paren_start..].find(')') {
let version_str = &trimmed[paren_start + 3..paren_start + paren_end].trim();
if !version_str.is_empty() {
version = version_str.to_string();
}
}
} else {
if let Some(equals_pos) = trimmed.find(" = ") {
provide_name = trimmed[..equals_pos].trim_end().to_string();
version = trimmed[equals_pos + 3..].trim().to_string();
}
}
} else if let Some(equals_pos) = trimmed.find(" = ") {
provide_name = trimmed[..equals_pos].trim_end().to_string();
version = trimmed[equals_pos + 3..].trim().to_string();
}
if !provide_name.is_empty() {
result.insert(provide_name, version);
}
}
result
}
fn rpm_token_equals_inside_parens(pos: usize, text: &str) -> bool {
let before = &text[..pos];
let mut depth = 0;
for ch in before.chars() {
if ch == '(' {
depth += 1;
} else if ch == ')' {
depth -= 1;
}
}
depth > 0
}
fn parse_rpm_provide_item(item: &str) -> Option<(String, String)> {
let parts: Vec<&str> = item.split_whitespace().collect();
if parts.is_empty() {
return None;
}
let mut provide_name: Option<String> = None;
let mut version = String::new();
for (idx, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if part == &">=" || part == &"<=" || part == &">" || part == &"<" || part == &"=" {
if idx + 1 < parts.len() {
version = parts[idx + 1].to_string();
}
break;
} else if let Some(i) = part.find(">=") {
if !rpm_token_equals_inside_parens(i, part) {
provide_name = Some(part[..i].to_string());
version = part[i + 2..].trim().to_string();
break;
} else {
provide_name = Some(part.to_string());
}
} else if let Some(i) = part.find("<=") {
if !rpm_token_equals_inside_parens(i, part) {
provide_name = Some(part[..i].to_string());
version = part[i + 2..].trim().to_string();
break;
} else {
provide_name = Some(part.to_string());
}
} else if let Some(i) = part.find('>') {
if !rpm_token_equals_inside_parens(i, part) {
provide_name = Some(part[..i].to_string());
version = part[i + 1..].trim().to_string();
break;
} else {
provide_name = Some(part.to_string());
}
} else if let Some(i) = part.find('<') {
if !rpm_token_equals_inside_parens(i, part) {
provide_name = Some(part[..i].to_string());
version = part[i + 1..].trim().to_string();
break;
} else {
provide_name = Some(part.to_string());
}
} else if let Some(i) = part.find('=') {
if rpm_token_equals_inside_parens(i, part) {
provide_name = Some(part.to_string());
} else {
provide_name = Some(part[..i].to_string());
version = part[i + 1..].trim().to_string();
break;
}
} else {
if provide_name.is_none() {
provide_name = Some(part.to_string());
}
}
}
provide_name.map(|name| (name, version))
}
fn parse_provides_rpm(provides_str: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
for item in provides_str.split(',') {
let item = item.trim();
if item.is_empty() {
continue;
}
if let Some((name, version)) = parse_rpm_provide_item(item) {
result.insert(name, version);
}
}
result
}
fn parse_provides_whitespace_equals(provides_str: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
for part in provides_str.split_whitespace() {
if part.is_empty() {
continue;
}
if let Some(i) = part.find('=') {
let name = part[..i].to_string();
let version = part[i + 1..].trim().to_string();
result.insert(name, version);
} else {
result.insert(part.to_string(), String::new());
}
}
result
}
pub fn parse_provides(provides_str: &str, format: PackageFormat) -> HashMap<String, String> {
match format {
PackageFormat::Apk | PackageFormat::Pacman => parse_provides_apk_pacman(provides_str),
PackageFormat::Deb => parse_provides_deb(provides_str),
PackageFormat::Rpm => parse_provides_rpm(provides_str),
_ => parse_provides_whitespace_equals(provides_str),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_provides_rpm() {
let provides = "filesystem = 3.16-6.oe2403sp1, filesystem(x86-64) = 3.16-6.oe2403sp1, filesystem-afs = 3.16-6.oe2403sp1";
let result = parse_provides(provides, PackageFormat::Rpm);
assert_eq!(result.get("filesystem"), Some(&"3.16-6.oe2403sp1".to_string()));
assert_eq!(result.get("filesystem(x86-64)"), Some(&"3.16-6.oe2403sp1".to_string()));
assert_eq!(result.get("filesystem-afs"), Some(&"3.16-6.oe2403sp1".to_string()));
assert_eq!(result.len(), 3);
}
#[test]
fn test_parse_provides_rpm_with_libtcl() {
let provides = "libtcl8.6.so()(64bit), tcl = 1:8.6.14-1.oe2403sp1, tcl(abi) = 8.6, tcl(x86-64) = 1:8.6.14-1.oe2403sp1, tcl-tcldict = 8.6.14";
let result = parse_provides(provides, PackageFormat::Rpm);
assert_eq!(result.get("libtcl8.6.so()(64bit)"), Some(&"".to_string()));
assert_eq!(result.get("tcl"), Some(&"1:8.6.14-1.oe2403sp1".to_string()));
assert_eq!(result.get("tcl(abi)"), Some(&"8.6".to_string()));
assert_eq!(result.get("tcl(x86-64)"), Some(&"1:8.6.14-1.oe2403sp1".to_string()));
assert_eq!(result.get("tcl-tcldict"), Some(&"8.6.14".to_string()));
assert_eq!(result.len(), 5);
}
#[test]
fn test_parse_provides_rpm_file_paths() {
let provides = "/etc/xdg/autostart, /etc/xdg/autostart/polkit-ukui-authentication-agent-1.desktop";
let result = parse_provides(provides, PackageFormat::Rpm);
assert_eq!(result.get("/etc/xdg/autostart"), Some(&"".to_string()));
assert_eq!(result.get("/etc/xdg/autostart/polkit-ukui-authentication-agent-1.desktop"), Some(&"".to_string()));
assert_eq!(result.len(), 2);
}
#[test]
fn test_parse_provides_pacman() {
let provides = "libutil-linux libblkid.so=1-64 libfdisk.so=1-64 libmount.so=1-64 libsmartcols.so=1-64 libuuid.so=1-64";
let result = parse_provides(provides, PackageFormat::Pacman);
assert_eq!(result.get("libutil-linux"), Some(&"".to_string()));
assert_eq!(result.get("libblkid.so"), Some(&"1-64".to_string()));
assert_eq!(result.get("libfdisk.so"), Some(&"1-64".to_string()));
assert_eq!(result.get("libmount.so"), Some(&"1-64".to_string()));
assert_eq!(result.get("libsmartcols.so"), Some(&"1-64".to_string()));
assert_eq!(result.get("libuuid.so"), Some(&"1-64".to_string()));
assert_eq!(result.len(), 6);
}
#[test]
fn test_parse_provides_apk() {
let provides = "pc:gio-2.0=2.84.4 pc:gio-unix-2.0=2.84.4 pc:girepository-2.0=2.84.4 pc:glib-2.0=2.84.4 pc:gmodule-2.0=2.84.4 pc:gmodule-export-2.0=2.84.4 pc:gmodule-no-export-2.0=2.84.4 pc:gobject-2.0=2.84.4 pc:gthread-2.0=2.84.4 cmd:gdbus-codegen=2.84.4-r0 cmd:glib-compile-resources=2.84.4-r0 cmd:glib-genmarshal=2.84.4-r0 cmd:glib-gettextize=2.84.4-r0 cmd:glib-mkenums=2.84.4-r0 cmd:gobject-query=2.84.4-r0 cmd:gresource=2.84.4-r0 cmd:gtester-report=2.84.4-r0 cmd:gtester=2.84.4-r0";
let result = parse_provides(provides, PackageFormat::Apk);
assert_eq!(result.get("pc:gio-2.0"), Some(&"2.84.4".to_string()));
assert_eq!(result.get("pc:gio-unix-2.0"), Some(&"2.84.4".to_string()));
assert_eq!(result.get("cmd:gdbus-codegen"), Some(&"2.84.4-r0".to_string()));
assert_eq!(result.get("cmd:gtester"), Some(&"2.84.4-r0".to_string()));
assert_eq!(result.len(), 18);
}
#[test]
fn test_parse_provides_apk_library_alias() {
let provides = "libstk-5.0.0.so=libstk-5.0.0.so-64";
let result = parse_provides(provides, PackageFormat::Apk);
assert_eq!(result.get("libstk-5.0.0.so"), Some(&"".to_string()));
assert_eq!(result.get("libstk-5.0.0.so-64"), Some(&"".to_string()));
assert_eq!(result.len(), 2);
}
#[test]
fn test_parse_provides_deb() {
let provides = "node-acorn-bigint (= 1.0.0), node-acorn-class-fields (= 1.0.0), node-acorn-dynamic-import (= 4.0.0), node-acorn-export-ns-from (= 0.2.0), node-acorn-globals (= 6.0.0), node-acorn-import-assertions (= 1.8.0), node-acorn-import-meta (= 1.1.0), node-acorn-jsx (= 5.3.1), node-acorn-loose (= 8.3.0), node-acorn-node (= 2.0.1), node-acorn-numeric-separator (= 0.3.4), node-acorn-private-class-elements (= 1.0.0), node-acorn-private-methods (= 1.0.0), node-acorn-static-class-features (= 1.0.0), node-acorn-walk (= 8.2.0), node-debbundle-acorn (= 8.8.1+ds+~cs25.17.7-2)";
let result = parse_provides(provides, PackageFormat::Deb);
assert_eq!(result.get("node-acorn-bigint"), Some(&"1.0.0".to_string()));
assert_eq!(result.get("node-acorn-class-fields"), Some(&"1.0.0".to_string()));
assert_eq!(result.get("node-acorn-dynamic-import"), Some(&"4.0.0".to_string()));
assert_eq!(result.get("node-debbundle-acorn"), Some(&"8.8.1+ds+~cs25.17.7-2".to_string()));
assert_eq!(result.len(), 16);
}
#[test]
fn test_parse_provides_deb_multiline() {
let provides = "tex4ht (= 2024.20250309-2),\n tex4ht-common (= 2024.20250309-2)";
let result = parse_provides(provides, PackageFormat::Deb);
assert_eq!(result.get("tex4ht"), Some(&"2024.20250309-2".to_string()));
assert_eq!(result.get("tex4ht-common"), Some(&"2024.20250309-2".to_string()));
assert_eq!(result.len(), 2);
}
#[test]
fn test_parse_provides_deb_with_arch() {
let provides = "libfoo (x86_64) = 2.0";
let result = parse_provides(provides, PackageFormat::Deb);
assert_eq!(result.get("libfoo (x86_64)"), Some(&"2.0".to_string()));
assert_eq!(result.len(), 1);
}
#[test]
fn test_parse_provides_empty() {
let result_rpm = parse_provides("", PackageFormat::Rpm);
let result_apk = parse_provides("", PackageFormat::Apk);
let result_deb = parse_provides("", PackageFormat::Deb);
let result_pacman = parse_provides("", PackageFormat::Pacman);
assert!(result_rpm.is_empty());
assert!(result_apk.is_empty());
assert!(result_deb.is_empty());
assert!(result_pacman.is_empty());
}
#[test]
fn test_parse_provides_rpm_preserves_arch() {
let provides = "libfoo(x86-64) = 2.0, libbar(any) = 1.0";
let result = parse_provides(provides, PackageFormat::Rpm);
assert_eq!(result.get("libfoo(x86-64)"), Some(&"2.0".to_string()));
assert_eq!(result.get("libbar(any)"), Some(&"1.0".to_string()));
assert!(!result.contains_key("libfoo"));
}
#[test]
fn test_parse_provides_rpm_complex_operators() {
let provides = "pkg1 >= 1.0, pkg2 <= 2.0, pkg3 > 0.5, pkg4 < 3.0, pkg5 = 1.2.3";
let result = parse_provides(provides, PackageFormat::Rpm);
assert_eq!(result.get("pkg1"), Some(&"1.0".to_string()));
assert_eq!(result.get("pkg2"), Some(&"2.0".to_string()));
assert_eq!(result.get("pkg3"), Some(&"0.5".to_string()));
assert_eq!(result.get("pkg4"), Some(&"3.0".to_string()));
assert_eq!(result.get("pkg5"), Some(&"1.2.3".to_string()));
assert_eq!(result.len(), 5);
}
#[test]
fn test_parse_provides_rpm_font_lang_capability() {
let provides = "font(:lang=he)";
let result = parse_provides(provides, PackageFormat::Rpm);
assert_eq!(result.get("font(:lang=he)"), Some(&"".to_string()));
assert_eq!(result.len(), 1);
}
}