use crate::{
Certainty, GuesserSettings, Person, ProviderError, UpstreamDatum, UpstreamDatumWithMetadata,
UpstreamMetadata,
};
use serde::Deserialize;
use std::collections::HashMap;
#[cfg(feature = "cargo")]
pub fn guess_from_cargo(
path: &std::path::Path,
_settings: &GuesserSettings,
) -> std::result::Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
let mut manifest = cargo_toml::Manifest::from_path(path)
.map_err(|e| ProviderError::ParseError(e.to_string()))?;
if manifest.needs_workspace_inheritance() {
if let Some(workspace_path) = path.parent().and_then(|p| p.parent()) {
let workspace_toml = workspace_path.join("Cargo.toml");
if workspace_toml.exists() {
if let Ok(workspace_manifest) = cargo_toml::Manifest::from_path(&workspace_toml) {
let _ = manifest.complete_from_path_and_workspace(
path,
Some((&workspace_manifest, workspace_path)),
);
}
}
}
}
let package = match manifest.package {
Some(ref p) => p,
None => {
log::debug!("No package section in Cargo.toml");
return Ok(Vec::new());
}
};
let mut results = Vec::new();
let name = &package.name;
results.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Name(name.clone()),
certainty: Some(Certainty::Certain),
origin: Some(path.into()),
});
results.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::CargoCrate(name.clone()),
certainty: Some(Certainty::Certain),
origin: Some(path.into()),
});
if let Some(description) = package.description() {
results.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Summary(description.to_string()),
certainty: Some(Certainty::Certain),
origin: Some(path.into()),
});
}
if let Some(homepage) = package.homepage() {
results.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Homepage(homepage.to_string()),
certainty: Some(Certainty::Certain),
origin: Some(path.into()),
});
}
if let Some(license) = package.license() {
results.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::License(license.to_string()),
certainty: Some(Certainty::Certain),
origin: Some(path.into()),
});
}
if let Some(repository) = package.repository() {
results.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Repository(repository.to_string()),
certainty: Some(Certainty::Certain),
origin: Some(path.into()),
});
}
let version = package.version();
results.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Version(version.to_string()),
certainty: Some(Certainty::Certain),
origin: Some(path.into()),
});
let authors = package.authors();
if !authors.is_empty() {
let authors = authors.iter().map(|a| Person::from(a.as_str())).collect();
results.push(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Author(authors),
certainty: Some(Certainty::Certain),
origin: Some(path.into()),
});
}
Ok(results)
}
pub async fn cargo_translate_dashes(
crate_name: &str,
) -> Result<Option<String>, crate::HTTPJSONError> {
let url: url::Url = format!("https://crates.io/api/v1/crates?q={}", crate_name)
.parse()
.expect("static crates.io URL template should always be valid");
let json: serde_json::Value = crate::load_json_url(&url, None).await?;
if let Some(crates) = json.get("crates").and_then(|c| c.as_array()) {
for krate in crates {
if let Some(name) = krate.get("id").and_then(|n| n.as_str()) {
return Ok(Some(name.to_string()));
}
}
}
Ok(None)
}
#[derive(Deserialize)]
pub struct Crate {
pub badges: Vec<String>,
pub created_at: String,
pub description: Option<String>,
pub documentation: Option<String>,
pub downloads: i64,
pub homepage: Option<String>,
pub id: String,
pub keywords: Vec<String>,
pub license: Option<String>,
pub links: HashMap<String, Option<String>>,
pub max_stable_version: semver::Version,
pub max_version: semver::Version,
pub name: String,
pub newest_version: semver::Version,
pub recent_downloads: i64,
pub repository: Option<String>,
pub updated_at: String,
pub versions: Option<Vec<i32>>,
}
#[derive(Deserialize)]
pub struct User {
pub avatar: String,
pub id: i32,
pub login: String,
pub name: String,
pub url: String,
}
#[derive(Deserialize)]
pub struct AuditAction {
pub action: String,
pub time: String,
pub user: User,
}
#[derive(Deserialize)]
pub struct CrateVersion {
pub audit_actions: Vec<AuditAction>,
pub bin_names: Vec<String>,
pub checksum: String,
#[serde(rename = "crate")]
pub crate_: String,
pub crate_size: i64,
pub created_at: String,
pub dl_path: String,
pub downloads: i64,
pub features: HashMap<String, Vec<String>>,
pub has_lib: bool,
pub id: i32,
pub lib_links: Option<HashMap<String, String>>,
pub license: Option<String>,
pub links: HashMap<String, Option<String>>,
pub num: semver::Version,
pub published_by: Option<User>,
pub readme_path: String,
pub rust_version: Option<String>,
pub updated_at: String,
pub yanked: bool,
}
#[derive(Deserialize)]
pub struct Category {
pub id: String,
pub category: String,
}
#[derive(Deserialize)]
pub struct Keyword {
pub id: String,
pub keyword: String,
}
#[derive(Deserialize)]
pub struct CrateInfo {
pub categories: Vec<Category>,
#[serde(rename = "crate")]
crate_: Crate,
pub keywords: Vec<Keyword>,
pub versions: Vec<CrateVersion>,
}
impl TryFrom<CrateInfo> for UpstreamMetadata {
type Error = crate::ProviderError;
fn try_from(value: CrateInfo) -> Result<Self, Self::Error> {
let mut ret = UpstreamMetadata::default();
ret.insert(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Name(value.crate_.name.to_string()),
certainty: Some(Certainty::Certain),
origin: None,
});
if let Some(homepage) = value.crate_.homepage {
ret.insert(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Homepage(homepage),
certainty: Some(Certainty::Certain),
origin: None,
});
}
if let Some(repository) = value.crate_.repository {
ret.insert(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Repository(repository),
certainty: Some(Certainty::Certain),
origin: None,
});
}
if let Some(description) = value.crate_.description {
ret.insert(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Summary(description),
certainty: Some(Certainty::Certain),
origin: None,
});
}
if let Some(license) = value.crate_.license {
ret.insert(UpstreamDatumWithMetadata {
datum: UpstreamDatum::License(license),
certainty: Some(Certainty::Certain),
origin: None,
});
}
ret.insert(UpstreamDatumWithMetadata {
datum: UpstreamDatum::Version(value.crate_.newest_version.to_string()),
certainty: Some(Certainty::Certain),
origin: None,
});
Ok(ret)
}
}
pub async fn load_crate_info(cratename: &str) -> Result<Option<CrateInfo>, crate::ProviderError> {
let http_url: url::Url = format!("https://crates.io/api/v1/crates/{}", cratename)
.parse()
.map_err(|e: url::ParseError| crate::ProviderError::Other(e.to_string()))?;
let data = crate::load_json_url(&http_url, None).await?;
serde_json::from_value(data)
.map(Some)
.map_err(|e| crate::ProviderError::ParseError(format!("Failed to parse crate data: {}", e)))
}
#[derive(Deserialize, Debug, Clone)]
pub struct IndexDependency {
pub name: String,
pub req: String,
#[serde(default)]
pub features: Vec<String>,
#[serde(default)]
pub optional: bool,
#[serde(default = "default_true")]
pub default_features: bool,
#[serde(default)]
pub target: Option<String>,
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
pub registry: Option<String>,
#[serde(default)]
pub package: Option<String>,
}
fn default_true() -> bool {
true
}
#[derive(Deserialize, Debug, Clone)]
pub struct IndexEntry {
pub name: String,
pub vers: semver::Version,
pub deps: Vec<IndexDependency>,
pub cksum: String,
#[serde(default)]
pub features: HashMap<String, Vec<String>>,
#[serde(default)]
pub features2: Option<HashMap<String, Vec<String>>>,
#[serde(default)]
pub yanked: bool,
#[serde(default)]
pub links: Option<String>,
#[serde(default)]
pub v: Option<u32>,
#[serde(default)]
pub rust_version: Option<String>,
}
fn index_path(name: &str) -> String {
let lower = name.to_lowercase();
match lower.len() {
1 => format!("1/{}", lower),
2 => format!("2/{}", lower),
3 => format!("3/{}/{}", &lower[..1], lower),
_ => format!("{}/{}/{}", &lower[..2], &lower[2..4], lower),
}
}
pub async fn load_index_info(
cratename: &str,
) -> Result<Option<Vec<IndexEntry>>, crate::ProviderError> {
let body = match crate::github::download_raw_file(
"rust-lang",
"crates.io-index",
"master",
&index_path(cratename),
)
.await
{
Ok(body) => body,
Err(crate::HTTPJSONError::Error { status: 404, .. }) => return Ok(None),
Err(e) => return Err(e.into()),
};
let mut entries = Vec::new();
for line in body.lines() {
if line.trim().is_empty() {
continue;
}
let entry: IndexEntry = serde_json::from_str(line).map_err(|e| {
crate::ProviderError::ParseError(format!("Failed to parse index entry: {}", e))
})?;
entries.push(entry);
}
Ok(Some(entries))
}
fn parse_crates_io(data: &CrateInfo) -> Vec<UpstreamDatum> {
let crate_data = &data.crate_;
let mut results = Vec::new();
results.push(UpstreamDatum::Name(crate_data.name.to_string()));
if let Some(homepage) = crate_data.homepage.as_ref() {
results.push(UpstreamDatum::Homepage(homepage.to_string()));
}
if let Some(repository) = crate_data.repository.as_ref() {
results.push(UpstreamDatum::Repository(repository.to_string()));
}
if let Some(description) = crate_data.description.as_ref() {
results.push(UpstreamDatum::Summary(description.to_string()));
}
if let Some(license) = crate_data.license.as_ref() {
results.push(UpstreamDatum::License(license.to_string()));
}
results.push(UpstreamDatum::Version(
crate_data.newest_version.to_string(),
));
results
}
pub struct CratesIo;
impl Default for CratesIo {
fn default() -> Self {
Self::new()
}
}
impl CratesIo {
pub fn new() -> Self {
Self
}
}
#[async_trait::async_trait]
impl crate::ThirdPartyRepository for CratesIo {
fn name(&self) -> &'static str {
"crates.io"
}
fn max_supported_certainty(&self) -> Certainty {
Certainty::Certain
}
fn supported_fields(&self) -> &'static [&'static str] {
&["Homepage", "Name", "Repository", "Version", "Summary"][..]
}
async fn guess_metadata(&self, name: &str) -> Result<Vec<UpstreamDatum>, ProviderError> {
let data = load_crate_info(name).await?;
if data.is_none() {
return Ok(Vec::new());
}
Ok(parse_crates_io(&data.unwrap()))
}
}
pub async fn remote_crate_data(name: &str) -> Result<UpstreamMetadata, crate::ProviderError> {
let data = load_crate_info(name).await?;
if let Some(data) = data {
Ok(data.try_into()?)
} else {
Ok(UpstreamMetadata::default())
}
}
#[cfg(test)]
mod crates_io_tests {
use super::*;
#[test]
fn test_load_crate_info() {
let data = include_str!("../testdata/crates.io.json");
let crate_info: CrateInfo = serde_json::from_str(data).unwrap();
assert_eq!(crate_info.crate_.name, "breezy");
}
#[test]
fn test_index_path() {
assert_eq!(index_path("a"), "1/a");
assert_eq!(index_path("ab"), "2/ab");
assert_eq!(index_path("abc"), "3/a/abc");
assert_eq!(index_path("serde"), "se/rd/serde");
assert_eq!(index_path("Inflector"), "in/fl/inflector");
assert_eq!(index_path("cargo-edit"), "ca/rg/cargo-edit");
}
#[test]
fn test_parse_index_entry() {
let line = r#"{"name":"serde","vers":"1.0.0","deps":[{"name":"serde_derive","req":"^1.0","features":[],"optional":true,"default_features":true,"target":null,"kind":"normal"}],"cksum":"00000000000000000000000000000000000000000000000000000000000000aa","features":{"derive":["serde_derive"],"default":["std"]},"yanked":false}"#;
let entry: IndexEntry = serde_json::from_str(line).unwrap();
assert_eq!(entry.name, "serde");
assert_eq!(entry.vers, semver::Version::new(1, 0, 0));
assert_eq!(entry.deps.len(), 1);
assert_eq!(entry.deps[0].name, "serde_derive");
assert!(entry.deps[0].optional);
assert!(!entry.yanked);
}
}