use binstalk_downloader::remote::{Client, Error as RemoteError, Url};
use binstalk_types::cargo_toml_binstall::Meta;
use cargo_toml_workspace::cargo_toml::Manifest;
use compact_str::{CompactString, ToCompactString};
use semver::{Comparator, Op as ComparatorOp, Version as SemVersion, VersionReq};
use serde::Deserialize;
use tracing::{debug, instrument};
use crate::{parse_manifest, MatchedVersion, RegistryError};
/// Return `Some(checksum)` if the version is not yanked, otherwise `None`.
async fn is_crate_yanked(client: &Client, url: Url) -> Result, RemoteError> {
#[derive(Deserialize)]
struct CrateInfo {
version: Inner,
}
#[derive(Deserialize)]
struct Inner {
yanked: bool,
checksum: String,
}
// Fetch / update index
debug!("Looking up crate information");
let info: CrateInfo = client.get(url).send(true).await?.json().await?;
let version = info.version;
Ok((!version.yanked).then_some(version.checksum))
}
async fn fetch_crate_cratesio_version_matched(
client: &Client,
url: Url,
version_req: &VersionReq,
) -> Result , RemoteError> {
#[derive(Deserialize)]
struct CrateInfo {
#[serde(rename = "crate")]
inner: CrateInfoInner,
versions: Vec,
}
#[derive(Deserialize)]
struct CrateInfoInner {
max_stable_version: CompactString,
}
#[derive(Deserialize)]
struct Version {
num: CompactString,
yanked: bool,
checksum: String,
}
// Fetch / update index
debug!("Looking up crate information");
let crate_info: CrateInfo = client.get(url).send(true).await?.json().await?;
let version_with_checksum = if version_req == &VersionReq::STAR {
let version = crate_info.inner.max_stable_version;
crate_info
.versions
.into_iter()
.find_map(|v| (v.num.as_str() == version.as_str()).then_some(v.checksum))
.map(|checksum| (version, checksum))
} else {
crate_info
.versions
.into_iter()
.filter_map(|item| {
if !item.yanked {
// Remove leading `v` for git tags
let num = if let Some(num) = item.num.strip_prefix('v') {
num.into()
} else {
item.num
};
// Parse out version
let ver = semver::Version::parse(&num).ok()?;
// Filter by version match
version_req
.matches(&ver)
.then_some((num, ver, item.checksum))
} else {
None
}
})
// Return highest version
.max_by(
|(_ver_str_x, ver_x, _checksum_x), (_ver_str_y, ver_y, _checksum_y)| {
ver_x.cmp(ver_y)
},
)
.map(|(ver_str, _, checksum)| (ver_str, checksum))
};
Ok(version_with_checksum)
}
/// Find the crate by name, get its latest stable version matches `version_req`,
/// retrieve its Cargo.toml and infer all its bins.
#[instrument]
pub async fn fetch_crate_cratesio_api(
client: Client,
name: &str,
version_req: &VersionReq,
) -> Result, RegistryError> {
let url = Url::parse(&format!("https://crates.io/api/v1/crates/{name}"))?;
let (version, cksum) = match version_req.comparators.as_slice() {
[Comparator {
op: ComparatorOp::Exact,
major,
minor: Some(minor),
patch: Some(patch),
pre,
}] => {
let version = SemVersion {
major: *major,
minor: *minor,
patch: *patch,
pre: pre.clone(),
build: Default::default(),
}
.to_compact_string();
let mut url = url.clone();
url.path_segments_mut().unwrap().push(&version);
is_crate_yanked(&client, url)
.await
.map(|ret| ret.map(|checksum| (version, checksum)))
}
_ => fetch_crate_cratesio_version_matched(&client, url.clone(), version_req).await,
}
.map_err(|e| match e {
RemoteError::Http(e) if e.is_status() => RegistryError::NotFound(name.into()),
e => e.into(),
})?
.ok_or_else(|| RegistryError::VersionMismatch {
req: version_req.clone(),
})?;
debug!("Found information for crate version: '{version}'");
// Download crate to temporary dir (crates.io or git?)
let mut crate_url = url;
crate_url
.path_segments_mut()
.unwrap()
.push(&version)
.push("download");
parse_manifest(client, name, crate_url, MatchedVersion { version, cksum }).await
}