//! Package capability checking and normalization
//!
//! This module handles checking whether packages provide specific capabilities
//! and normalizing capability names. Key features:
//! - Capability matching against package provides (direct and bundled)
//! - Support for Fedora Bundled Software Policy (bundled() capabilities)
//! - Architecture-aware capability normalization (RPM vs Debian formats)
//! - File-based capability checking for absolute paths
use crate::parse_provides::parse_provides;
use crate::models::PackageFormat;
use crate::resolve::provider::GenericDependencyProvider;
impl GenericDependencyProvider {
/// Check if a package provides a given capability
///
/// IMPORTANT: The capability parameter should be cap_with_arch (e.g., "libfoo(x86-64)")
/// if it includes architecture information. The function strips version constraints
/// but preserves cap_with_arch, which is then matched against provide names that
/// are also cap_with_arch (preserved by parse_provides).
///
/// ## Bundled Software Support (Fedora Policy)
///
/// This function also checks for `bundled()` variants. According to Fedora's
/// Bundled Software Policy (https://docs.fedoraproject.org/en-US/fesco/Bundled_Software_policy/),
/// packages that bundle libraries must include `Provides: bundled(library) = version`.
///
/// When checking if a package provides a capability, this function will match both:
/// - Direct provides: if package provides "php-composer(doctrine/cache)"
/// - Bundled provides: if package provides "bundled(php-composer(doctrine/cache))"
///
/// This ensures that packages providing bundled libraries can satisfy dependencies
/// that require the unbundled capability name.
pub fn package_provides_capability(&self, pkgkey: &str, capability: &str) -> bool {
// Try to load package info
let package = match self.load_package_for_solvable(pkgkey) {
Ok(pkg) => pkg,
Err(_) => return false,
};
// Strip version constraints from capability (if any)
// This preserves cap_with_arch (e.g., "libfoo(x86-64)=2.0" -> "libfoo(x86-64)")
let (cap_without_version, _) =
crate::parse_requires::parse_package_spec_with_version(capability, self.format);
// Check if any provide matches the capability
// provide_map from parse_provides contains cap_with_arch keys (atomic, never split)
// Also check for bundled() variants: if looking for "cap", also check "bundled(cap)"
let bundled_variant = format!("bundled({})", cap_without_version);
for provide_str in &package.provides {
let provide_map = parse_provides(provide_str, self.format);
for (provide_name, _version) in provide_map {
// Both provide_name and cap_without_version are cap_with_arch (atomic)
// Check direct match
if provide_name == cap_without_version {
log::debug!(
"[RESOLVO] package_provides_capability: {} provides '{}' via provide '{}'",
pkgkey,
cap_without_version,
provide_str
);
return true;
}
// Check bundled variant: if package provides "bundled(cap)", it also provides "cap"
if provide_name == bundled_variant {
log::debug!(
"[RESOLVO] package_provides_capability: {} provides '{}' via bundled provide '{}'",
pkgkey,
cap_without_version,
provide_str
);
return true;
}
}
}
// If capability looks like a file path (starts with '/'), also check the files field
if cap_without_version.starts_with('/') {
for file_path in &package.files {
// Check for exact match
if file_path == &cap_without_version {
log::debug!(
"[RESOLVO] package_provides_capability: {} provides '{}' via file '{}'",
pkgkey,
cap_without_version,
file_path
);
return true;
}
}
}
log::trace!(
"[RESOLVO] package_provides_capability: {} does NOT provide '{}'",
pkgkey,
cap_without_version
);
false
}
/// Normalize capability name by stripping architecture suffixes like `:any`
/// Returns the base capability name used for package/provide lookup
///
/// IMPORTANT: This function handles `:any` style arch suffixes (Debian format),
/// but for RPM-style `(arch)` suffixes, the capability is cap_with_arch which
/// should NOT be split. When capability contains `(arch)`, we preserve it as-is
/// for provide lookups, as provide2pkgnames is keyed by cap_with_arch.
pub fn normalize_capability_name(&self, capability: &str) -> String {
let (base_capability, arch_spec) =
crate::package::parse_capability_architecture(capability, self.format);
// If arch_spec is Some, it means we successfully parsed an arch suffix
// For RPM format with (arch), the capability is cap_with_arch and should
// be preserved as-is for provide lookups. Only strip :any style suffixes.
if base_capability.is_empty() {
capability.to_string()
} else if self.format == PackageFormat::Rpm && arch_spec.is_some() {
// RPM format: capability is cap_with_arch (e.g., "libfoo(x86-64)")
// Preserve it as-is for provide lookups, as provide2pkgnames is keyed by cap_with_arch
capability.to_string()
} else {
// Debian format with :any or other formats: use base_capability
// Note: For provides stored as cap_with_arch, this might not match,
// but :any dependencies are handled differently in Debian
base_capability
}
}
}