use anyhow::{Result, anyhow};
use std::path::Path;
pub struct CargoMetadata {
parsed: toml::Value,
}
impl CargoMetadata {
pub fn from_toml_str(s: &str) -> Result<Self> {
let parsed: toml::Value =
s.parse().map_err(|e| anyhow!("Failed to parse Cargo.toml: {e}"))?;
Ok(Self { parsed })
}
pub fn binary_names(&self) -> Vec<String> {
if let Some(bins) = self.parsed.get("bin").and_then(|b| b.as_array()) {
let names: Vec<String> = bins
.iter()
.filter_map(|b| b.get("name").and_then(|n| n.as_str()).map(String::from))
.collect();
if !names.is_empty() {
return names;
}
}
if let Some(name) =
self.parsed.get("package").and_then(|p| p.get("name")).and_then(|n| n.as_str())
{
vec![name.to_string()]
} else {
vec![]
}
}
pub fn workspace_members(&self) -> Vec<String> {
self.parsed
.get("workspace")
.and_then(|w| w.get("members"))
.and_then(|m| m.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default()
}
pub fn package_name(&self) -> Option<String> {
self.parsed
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.map(String::from)
}
pub fn is_workspace(&self) -> bool {
self.parsed.get("workspace").is_some()
}
pub fn has_package(&self) -> bool {
self.parsed.get("package").is_some()
}
pub fn version(&self, workspace_meta: Option<&Self>) -> Option<String> {
self.package_field_or_workspace("version", workspace_meta)
}
pub fn license(&self, workspace_meta: Option<&Self>) -> Option<String> {
self.package_field_or_workspace("license", workspace_meta)
}
pub fn description(&self, workspace_meta: Option<&Self>) -> Option<String> {
self.package_field_or_workspace("description", workspace_meta)
}
pub fn homepage(&self, workspace_meta: Option<&Self>) -> Option<String> {
self.package_field_or_workspace("homepage", workspace_meta)
}
pub fn repository(&self, workspace_meta: Option<&Self>) -> Option<String> {
self.package_field_or_workspace("repository", workspace_meta)
}
pub fn documentation(&self, workspace_meta: Option<&Self>) -> Option<String> {
self.package_field_or_workspace("documentation", workspace_meta)
}
fn package_field_or_workspace(
&self,
field: &str,
workspace_meta: Option<&Self>,
) -> Option<String> {
let pkg = self.parsed.get("package")?;
let val = pkg.get(field)?;
if let Some(table) = val.as_table() {
if table.get("workspace").and_then(|v| v.as_bool()) == Some(true) {
return workspace_meta.and_then(|ws| {
ws.parsed
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.get(field))
.and_then(|v| v.as_str())
.map(String::from)
});
}
}
val.as_str().map(String::from)
}
pub fn dependencies(&self) -> Vec<(String, bool)> {
Self::parse_dep_table(self.parsed.get("dependencies"))
}
pub fn build_dependencies(&self) -> Vec<String> {
Self::parse_dep_table(self.parsed.get("build-dependencies"))
.into_iter()
.map(|(name, _)| name)
.collect()
}
fn parse_dep_table(table: Option<&toml::Value>) -> Vec<(String, bool)> {
let Some(table) = table.and_then(|t| t.as_table()) else {
return vec![];
};
table
.iter()
.map(|(name, val)| {
let optional = val
.as_table()
.and_then(|t| t.get("optional"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
(name.clone(), optional)
})
.collect()
}
}
pub fn resolve_workspace_members(members: &[String], tree: &[String]) -> Vec<String> {
let mut result = Vec::new();
for member in members {
if member.contains('*') {
let prefix = member.split('*').next().unwrap_or("");
for path in tree {
if let Some(rest) = path.strip_prefix(prefix) {
if rest.ends_with("/Cargo.toml") && !rest[..rest.len() - 11].contains('/') {
let dir = &path[..path.len() - 11]; if !result.contains(&dir.to_string()) {
result.push(dir.to_string());
}
}
}
}
} else {
result.push(member.clone());
}
}
result
}
pub fn parse_cargo_lock_str(content: &str) -> Result<Vec<String>> {
let parsed: toml::Value =
content.parse().map_err(|e| anyhow!("Failed to parse Cargo.lock: {e}"))?;
let Some(packages) = parsed.get("package").and_then(|p| p.as_array()) else {
return Ok(vec![]);
};
Ok(packages
.iter()
.filter_map(|pkg| pkg.get("name").and_then(|n| n.as_str()).map(String::from))
.collect())
}
pub fn parse_cargo_lock(path: &Path) -> Result<Vec<String>> {
let content = std::fs::read_to_string(path)
.map_err(|e| anyhow!("Failed to read {}: {e}", path.display()))?;
parse_cargo_lock_str(&content)
}
pub fn detect_license_files(file_names: &[String]) -> Vec<String> {
let patterns = ["LICENSE", "LICENCE", "COPYING"];
let mut found: Vec<String> = file_names
.iter()
.filter(|f| {
if f.contains('/') {
return false;
}
let upper = f.to_uppercase();
patterns.iter().any(|p| upper.starts_with(p))
})
.cloned()
.collect();
found.sort();
found
}