1use crate::{
2 Certainty, GuesserSettings, Person, ProviderError, UpstreamDatum, UpstreamDatumWithMetadata,
3 UpstreamMetadata,
4};
5use serde::Deserialize;
6use std::collections::HashMap;
7
8#[cfg(feature = "cargo")]
10pub fn guess_from_cargo(
11 path: &std::path::Path,
12 _settings: &GuesserSettings,
13) -> std::result::Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
14 let mut manifest = cargo_toml::Manifest::from_path(path)
16 .map_err(|e| ProviderError::ParseError(e.to_string()))?;
17
18 if manifest.needs_workspace_inheritance() {
20 if let Some(workspace_path) = path.parent().and_then(|p| p.parent()) {
21 let workspace_toml = workspace_path.join("Cargo.toml");
22 if workspace_toml.exists() {
23 if let Ok(workspace_manifest) = cargo_toml::Manifest::from_path(&workspace_toml) {
24 let _ = manifest.complete_from_path_and_workspace(
25 path,
26 Some((&workspace_manifest, workspace_path)),
27 );
28 }
29 }
30 }
31 }
32
33 let package = match manifest.package {
34 Some(ref p) => p,
35 None => {
36 log::debug!("No package section in Cargo.toml");
37 return Ok(Vec::new());
38 }
39 };
40
41 let mut results = Vec::new();
42
43 let name = &package.name;
44 results.push(UpstreamDatumWithMetadata {
45 datum: UpstreamDatum::Name(name.clone()),
46 certainty: Some(Certainty::Certain),
47 origin: Some(path.into()),
48 });
49 results.push(UpstreamDatumWithMetadata {
50 datum: UpstreamDatum::CargoCrate(name.clone()),
51 certainty: Some(Certainty::Certain),
52 origin: Some(path.into()),
53 });
54
55 if let Some(description) = package.description() {
56 results.push(UpstreamDatumWithMetadata {
57 datum: UpstreamDatum::Summary(description.to_string()),
58 certainty: Some(Certainty::Certain),
59 origin: Some(path.into()),
60 });
61 }
62
63 if let Some(homepage) = package.homepage() {
64 results.push(UpstreamDatumWithMetadata {
65 datum: UpstreamDatum::Homepage(homepage.to_string()),
66 certainty: Some(Certainty::Certain),
67 origin: Some(path.into()),
68 });
69 }
70
71 if let Some(license) = package.license() {
72 results.push(UpstreamDatumWithMetadata {
73 datum: UpstreamDatum::License(license.to_string()),
74 certainty: Some(Certainty::Certain),
75 origin: Some(path.into()),
76 });
77 }
78
79 if let Some(repository) = package.repository() {
80 results.push(UpstreamDatumWithMetadata {
81 datum: UpstreamDatum::Repository(repository.to_string()),
82 certainty: Some(Certainty::Certain),
83 origin: Some(path.into()),
84 });
85 }
86
87 let version = package.version();
88 results.push(UpstreamDatumWithMetadata {
89 datum: UpstreamDatum::Version(version.to_string()),
90 certainty: Some(Certainty::Certain),
91 origin: Some(path.into()),
92 });
93
94 let authors = package.authors();
95 if !authors.is_empty() {
96 let authors = authors.iter().map(|a| Person::from(a.as_str())).collect();
97 results.push(UpstreamDatumWithMetadata {
98 datum: UpstreamDatum::Author(authors),
99 certainty: Some(Certainty::Certain),
100 origin: Some(path.into()),
101 });
102 }
103
104 Ok(results)
105}
106
107pub async fn cargo_translate_dashes(
109 crate_name: &str,
110) -> Result<Option<String>, crate::HTTPJSONError> {
111 let url: url::Url = format!("https://crates.io/api/v1/crates?q={}", crate_name)
112 .parse()
113 .expect("static crates.io URL template should always be valid");
114 let json: serde_json::Value = crate::load_json_url(&url, None).await?;
115
116 if let Some(crates) = json.get("crates").and_then(|c| c.as_array()) {
118 for krate in crates {
119 if let Some(name) = krate.get("id").and_then(|n| n.as_str()) {
120 return Ok(Some(name.to_string()));
121 }
122 }
123 }
124
125 Ok(None)
126}
127
128#[derive(Deserialize)]
130pub struct Crate {
131 pub badges: Vec<String>,
133 pub created_at: String,
135 pub description: Option<String>,
137 pub documentation: Option<String>,
139 pub downloads: i64,
141 pub homepage: Option<String>,
143 pub id: String,
145 pub keywords: Vec<String>,
147 pub license: Option<String>,
149 pub links: HashMap<String, Option<String>>,
151 pub max_stable_version: semver::Version,
153 pub max_version: semver::Version,
155 pub name: String,
157 pub newest_version: semver::Version,
159 pub recent_downloads: i64,
161 pub repository: Option<String>,
163 pub updated_at: String,
165 pub versions: Option<Vec<i32>>,
167}
168
169#[derive(Deserialize)]
171pub struct User {
172 pub avatar: String,
174 pub id: i32,
176 pub login: String,
178 pub name: String,
180 pub url: String,
182}
183
184#[derive(Deserialize)]
186pub struct AuditAction {
187 pub action: String,
189 pub time: String,
191 pub user: User,
193}
194
195#[derive(Deserialize)]
197pub struct CrateVersion {
198 pub audit_actions: Vec<AuditAction>,
200 pub bin_names: Vec<String>,
202 pub checksum: String,
204 #[serde(rename = "crate")]
206 pub crate_: String,
207 pub crate_size: i64,
209 pub created_at: String,
211 pub dl_path: String,
213 pub downloads: i64,
215 pub features: HashMap<String, Vec<String>>,
217 pub has_lib: bool,
219 pub id: i32,
221 pub lib_links: Option<HashMap<String, String>>,
223 pub license: Option<String>,
225 pub links: HashMap<String, Option<String>>,
227 pub num: semver::Version,
229 pub published_by: Option<User>,
231 pub readme_path: String,
233 pub rust_version: Option<String>,
235 pub updated_at: String,
237 pub yanked: bool,
239}
240
241#[derive(Deserialize)]
243pub struct Category {
244 pub id: String,
246 pub category: String,
248}
249
250#[derive(Deserialize)]
252pub struct Keyword {
253 pub id: String,
255 pub keyword: String,
257}
258
259#[derive(Deserialize)]
261pub struct CrateInfo {
262 pub categories: Vec<Category>,
264 #[serde(rename = "crate")]
265 crate_: Crate,
266 pub keywords: Vec<Keyword>,
268 pub versions: Vec<CrateVersion>,
270}
271
272impl TryFrom<CrateInfo> for UpstreamMetadata {
273 type Error = crate::ProviderError;
274
275 fn try_from(value: CrateInfo) -> Result<Self, Self::Error> {
276 let mut ret = UpstreamMetadata::default();
277
278 ret.insert(UpstreamDatumWithMetadata {
279 datum: UpstreamDatum::Name(value.crate_.name.to_string()),
280 certainty: Some(Certainty::Certain),
281 origin: None,
282 });
283
284 if let Some(homepage) = value.crate_.homepage {
285 ret.insert(UpstreamDatumWithMetadata {
286 datum: UpstreamDatum::Homepage(homepage),
287 certainty: Some(Certainty::Certain),
288 origin: None,
289 });
290 }
291
292 if let Some(repository) = value.crate_.repository {
293 ret.insert(UpstreamDatumWithMetadata {
294 datum: UpstreamDatum::Repository(repository),
295 certainty: Some(Certainty::Certain),
296 origin: None,
297 });
298 }
299
300 if let Some(description) = value.crate_.description {
301 ret.insert(UpstreamDatumWithMetadata {
302 datum: UpstreamDatum::Summary(description),
303 certainty: Some(Certainty::Certain),
304 origin: None,
305 });
306 }
307
308 if let Some(license) = value.crate_.license {
309 ret.insert(UpstreamDatumWithMetadata {
310 datum: UpstreamDatum::License(license),
311 certainty: Some(Certainty::Certain),
312 origin: None,
313 });
314 }
315
316 ret.insert(UpstreamDatumWithMetadata {
317 datum: UpstreamDatum::Version(value.crate_.newest_version.to_string()),
318 certainty: Some(Certainty::Certain),
319 origin: None,
320 });
321
322 Ok(ret)
323 }
324}
325
326pub async fn load_crate_info(cratename: &str) -> Result<Option<CrateInfo>, crate::ProviderError> {
328 let http_url: url::Url = format!("https://crates.io/api/v1/crates/{}", cratename)
329 .parse()
330 .map_err(|e: url::ParseError| crate::ProviderError::Other(e.to_string()))?;
331
332 let data = crate::load_json_url(&http_url, None).await?;
333
334 serde_json::from_value(data)
335 .map(Some)
336 .map_err(|e| crate::ProviderError::ParseError(format!("Failed to parse crate data: {}", e)))
337}
338
339#[derive(Deserialize, Debug, Clone)]
343pub struct IndexDependency {
344 pub name: String,
346 pub req: String,
348 #[serde(default)]
350 pub features: Vec<String>,
351 #[serde(default)]
353 pub optional: bool,
354 #[serde(default = "default_true")]
356 pub default_features: bool,
357 #[serde(default)]
359 pub target: Option<String>,
360 #[serde(default)]
362 pub kind: Option<String>,
363 #[serde(default)]
365 pub registry: Option<String>,
366 #[serde(default)]
368 pub package: Option<String>,
369}
370
371fn default_true() -> bool {
372 true
373}
374
375#[derive(Deserialize, Debug, Clone)]
382pub struct IndexEntry {
383 pub name: String,
385 pub vers: semver::Version,
387 pub deps: Vec<IndexDependency>,
389 pub cksum: String,
391 #[serde(default)]
393 pub features: HashMap<String, Vec<String>>,
394 #[serde(default)]
396 pub features2: Option<HashMap<String, Vec<String>>>,
397 #[serde(default)]
399 pub yanked: bool,
400 #[serde(default)]
402 pub links: Option<String>,
403 #[serde(default)]
405 pub v: Option<u32>,
406 #[serde(default)]
408 pub rust_version: Option<String>,
409}
410
411fn index_path(name: &str) -> String {
418 let lower = name.to_lowercase();
419 match lower.len() {
420 1 => format!("1/{}", lower),
421 2 => format!("2/{}", lower),
422 3 => format!("3/{}/{}", &lower[..1], lower),
423 _ => format!("{}/{}/{}", &lower[..2], &lower[2..4], lower),
424 }
425}
426
427pub async fn load_index_info(
437 cratename: &str,
438) -> Result<Option<Vec<IndexEntry>>, crate::ProviderError> {
439 let body = match crate::github::download_raw_file(
440 "rust-lang",
441 "crates.io-index",
442 "master",
443 &index_path(cratename),
444 )
445 .await
446 {
447 Ok(body) => body,
448 Err(crate::HTTPJSONError::Error { status: 404, .. }) => return Ok(None),
449 Err(e) => return Err(e.into()),
450 };
451
452 let mut entries = Vec::new();
453 for line in body.lines() {
454 if line.trim().is_empty() {
455 continue;
456 }
457 let entry: IndexEntry = serde_json::from_str(line).map_err(|e| {
458 crate::ProviderError::ParseError(format!("Failed to parse index entry: {}", e))
459 })?;
460 entries.push(entry);
461 }
462
463 Ok(Some(entries))
464}
465
466fn parse_crates_io(data: &CrateInfo) -> Vec<UpstreamDatum> {
468 let crate_data = &data.crate_;
469 let mut results = Vec::new();
470 results.push(UpstreamDatum::Name(crate_data.name.to_string()));
471 if let Some(homepage) = crate_data.homepage.as_ref() {
472 results.push(UpstreamDatum::Homepage(homepage.to_string()));
473 }
474 if let Some(repository) = crate_data.repository.as_ref() {
475 results.push(UpstreamDatum::Repository(repository.to_string()));
476 }
477 if let Some(description) = crate_data.description.as_ref() {
478 results.push(UpstreamDatum::Summary(description.to_string()));
479 }
480 if let Some(license) = crate_data.license.as_ref() {
481 results.push(UpstreamDatum::License(license.to_string()));
482 }
483 results.push(UpstreamDatum::Version(
484 crate_data.newest_version.to_string(),
485 ));
486
487 results
488}
489
490pub struct CratesIo;
492
493impl Default for CratesIo {
494 fn default() -> Self {
495 Self::new()
496 }
497}
498
499impl CratesIo {
500 pub fn new() -> Self {
502 Self
503 }
504}
505
506#[async_trait::async_trait]
507impl crate::ThirdPartyRepository for CratesIo {
508 fn name(&self) -> &'static str {
509 "crates.io"
510 }
511
512 fn max_supported_certainty(&self) -> Certainty {
513 Certainty::Certain
514 }
515
516 fn supported_fields(&self) -> &'static [&'static str] {
517 &["Homepage", "Name", "Repository", "Version", "Summary"][..]
518 }
519
520 async fn guess_metadata(&self, name: &str) -> Result<Vec<UpstreamDatum>, ProviderError> {
521 let data = load_crate_info(name).await?;
522 if data.is_none() {
523 return Ok(Vec::new());
524 }
525 Ok(parse_crates_io(&data.unwrap()))
526 }
527}
528
529pub async fn remote_crate_data(name: &str) -> Result<UpstreamMetadata, crate::ProviderError> {
531 let data = load_crate_info(name).await?;
532
533 if let Some(data) = data {
534 Ok(data.try_into()?)
535 } else {
536 Ok(UpstreamMetadata::default())
537 }
538}
539
540#[cfg(test)]
541mod crates_io_tests {
542 use super::*;
543
544 #[test]
545 fn test_load_crate_info() {
546 let data = include_str!("../testdata/crates.io.json");
547
548 let crate_info: CrateInfo = serde_json::from_str(data).unwrap();
549
550 assert_eq!(crate_info.crate_.name, "breezy");
551 }
552
553 #[test]
554 fn test_index_path() {
555 assert_eq!(index_path("a"), "1/a");
556 assert_eq!(index_path("ab"), "2/ab");
557 assert_eq!(index_path("abc"), "3/a/abc");
558 assert_eq!(index_path("serde"), "se/rd/serde");
559 assert_eq!(index_path("Inflector"), "in/fl/inflector");
560 assert_eq!(index_path("cargo-edit"), "ca/rg/cargo-edit");
561 }
562
563 #[test]
564 fn test_parse_index_entry() {
565 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}"#;
566
567 let entry: IndexEntry = serde_json::from_str(line).unwrap();
568
569 assert_eq!(entry.name, "serde");
570 assert_eq!(entry.vers, semver::Version::new(1, 0, 0));
571 assert_eq!(entry.deps.len(), 1);
572 assert_eq!(entry.deps[0].name, "serde_derive");
573 assert!(entry.deps[0].optional);
574 assert!(!entry.yanked);
575 }
576}