rustdoc_markdown/
cratesio.rs1use anyhow::{Context, Result, anyhow, bail};
2use flate2::read::GzDecoder;
3
4use semver::{Version, VersionReq};
5use serde::Deserialize;
6use std::io::Cursor; use std::path::{Path as FilePath, PathBuf}; use tar::Archive;
9use tracing::{debug, info, warn};
10
11#[derive(Deserialize, Debug)]
12struct CratesApiResponse {
13 versions: Vec<CrateVersion>,
14}
15
16#[derive(Deserialize, Debug, Clone)]
18pub struct CrateVersion {
19 #[serde(rename = "crate")]
21 pub crate_name: String,
22 pub num: String, pub yanked: bool,
26 #[serde(skip)]
28 pub semver: Option<Version>, }
30
31pub async fn find_best_version(
51 client: &reqwest::Client,
52 crate_name: &str,
53 version_req_str: &str,
54 include_prerelease: bool,
55) -> Result<CrateVersion> {
56 info!(
57 "Fetching versions for crate '{}' from crates.io...",
58 crate_name
59 );
60 let url = format!("https://crates.io/api/v1/crates/{}", crate_name);
61 let response = client.get(&url).send().await?.error_for_status()?;
62 let mut api_data: CratesApiResponse = response
63 .json()
64 .await
65 .context("Failed to parse JSON response from crates.io API")?;
66
67 if api_data.versions.is_empty() {
68 bail!("No versions found for crate '{}'", crate_name);
69 }
70
71 api_data.versions.retain_mut(|v| {
73 if v.yanked {
74 debug!("Ignoring yanked version: {}", v.num);
75 return false;
76 }
77 match Version::parse(&v.num) {
78 Ok(sv) => {
79 v.semver = Some(sv);
80 true
81 }
82 Err(e) => {
83 warn!("Failed to parse version '{}': {}", v.num, e);
84 false }
86 }
87 });
88
89 if !include_prerelease {
91 api_data
92 .versions
93 .retain(|v| v.semver.as_ref().is_some_and(|sv| sv.pre.is_empty()));
94 }
95
96 api_data
98 .versions
99 .sort_unstable_by(|a, b| b.semver.cmp(&a.semver)); if api_data.versions.is_empty() {
102 bail!(
103 "No suitable non-yanked{} versions found for crate '{}'",
104 if include_prerelease { "" } else { " stable" },
105 crate_name
106 );
107 }
108
109 match version_req_str {
110 "*" => {
111 info!("No version specified, selecting latest suitable version...");
113 api_data.versions.into_iter().next().ok_or_else(|| {
114 anyhow!(
115 "Could not determine the latest{} version for crate '{}'",
116 if include_prerelease { "" } else { " stable" },
117 crate_name
118 )
119 })
120 }
121 req_str => {
122 info!(
123 "Finding best match for version requirement '{}'...",
124 req_str
125 );
126 let req = VersionReq::parse(req_str)
127 .with_context(|| format!("Invalid version requirement string: '{}'", req_str))?;
128
129 api_data
130 .versions
131 .into_iter()
132 .find(|v| v.semver.as_ref().is_some_and(|sv| req.matches(sv)))
133 .ok_or_else(|| {
134 anyhow!(
135 "No version found matching requirement '{}' for crate '{}'",
136 req_str,
137 crate_name
138 )
139 })
140 }
141 }
142}
143
144pub async fn download_and_unpack_crate(
161 client: &reqwest::Client,
162 krate: &CrateVersion,
163 build_path: &FilePath, ) -> Result<PathBuf> {
165 let crate_dir_name = format!("{}-{}", krate.crate_name, krate.num);
166 let target_dir = build_path.join(crate_dir_name); if target_dir.exists() {
169 info!(
170 "Crate already downloaded and unpacked at: {}",
171 target_dir.display()
172 );
173 return Ok(target_dir);
174 }
175
176 info!("Downloading {} version {}...", krate.crate_name, krate.num);
177 let url = format!(
178 "https://crates.io/api/v1/crates/{}/{}/download",
179 krate.crate_name, krate.num
180 );
181 let response = client.get(&url).send().await?.error_for_status()?;
182
183 let content = response.bytes().await?;
184 let reader = Cursor::new(content); info!("Unpacking crate to: {}", target_dir.display());
187 std::fs::create_dir_all(&target_dir)
188 .with_context(|| format!("Failed to create directory: {}", target_dir.display()))?;
189
190 let tar = GzDecoder::new(reader);
191 let mut archive = Archive::new(tar);
192
193 let crate_dir_prefix = format!("{}-{}/", krate.crate_name, krate.num);
195
196 for entry_result in archive.entries()? {
197 let mut entry = entry_result?;
198 let path = entry.path()?;
199
200 if path.starts_with(&crate_dir_prefix) {
202 let relative_path = path.strip_prefix(&crate_dir_prefix)?;
203 let dest_path = target_dir.join(relative_path);
204
205 if entry.header().entry_type().is_dir() {
206 std::fs::create_dir_all(&dest_path)?;
207 } else {
208 if let Some(parent) = dest_path.parent() {
209 std::fs::create_dir_all(parent)?;
210 }
211 entry.unpack(&dest_path)?;
212 }
213 } else {
214 debug!("Skipping entry outside expected crate dir: {:?}", path);
215 }
216 }
217
218 info!("Unpacked to: {}", target_dir.display());
219 Ok(target_dir)
220}