cargo-binstall/crates/binstalk-registry/src/lib.rs
Jiahao XU b9adaa006f
binstalk-registry: Use crates.io sparse index by default (#1314)
Fixed #1310

Also add rename `fetch_crate_cratesio` => `fetch_crate_cratesio_api` and
put it behind a new feature `crates_io_api`.

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
2023-08-24 00:04:06 +00:00

297 lines
8.3 KiB
Rust

#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::{io, str::FromStr, sync::Arc};
use base16::DecodeError as Base16DecodeError;
use binstalk_downloader::{
download::DownloadError,
remote::{Client, Error as RemoteError},
};
use binstalk_types::cargo_toml_binstall::Meta;
use cargo_toml_workspace::cargo_toml::{Error as CargoTomlError, Manifest};
use compact_str::CompactString;
use leon::{ParseError, RenderError};
use miette::Diagnostic;
use semver::VersionReq;
use serde_json::Error as JsonError;
use thiserror::Error as ThisError;
use tokio::task;
use url::{ParseError as UrlParseError, Url};
#[cfg(feature = "git")]
pub use simple_git::{GitError, GitUrl, GitUrlParseError};
mod vfs;
mod visitor;
mod common;
use common::*;
#[cfg(feature = "git")]
mod git_registry;
#[cfg(feature = "git")]
pub use git_registry::GitRegistry;
#[cfg(any(feature = "crates_io_api", test))]
mod crates_io_registry;
#[cfg(any(feature = "crates_io_api", test))]
pub use crates_io_registry::fetch_crate_cratesio_api;
mod sparse_registry;
pub use sparse_registry::SparseRegistry;
#[derive(Debug, ThisError, Diagnostic)]
#[diagnostic(severity(error), code(binstall::cargo_registry))]
#[non_exhaustive]
pub enum RegistryError {
#[error(transparent)]
Remote(#[from] RemoteError),
#[error("{0} is not found")]
#[diagnostic(
help("Check that the crate name you provided is correct.\nYou can also search for a matching crate at: https://lib.rs/search?q={0}")
)]
NotFound(CompactString),
#[error(transparent)]
Json(#[from] JsonError),
#[error("Failed to parse dl config: {0}")]
ParseDlConfig(#[from] ParseError),
#[error("Failed to render dl config: {0}")]
RenderDlConfig(#[from] RenderError),
#[error("Failed to parse checksum encoded in hex: {0}")]
InvalidHex(#[from] Base16DecodeError),
#[error("Expected checksum `{expected}`, actual checksum `{actual}`")]
UnmatchedChecksum {
expected: Box<str>,
actual: Box<str>,
},
#[error("no version matching requirement '{req}'")]
VersionMismatch { req: semver::VersionReq },
#[error("Failed to parse cargo manifest: {0}")]
#[diagnostic(help("If you used --manifest-path, check the Cargo.toml syntax."))]
CargoManifest(#[from] Box<CargoTomlError>),
#[error("Failed to parse url: {0}")]
UrlParse(#[from] url::ParseError),
#[error(transparent)]
Download(#[from] DownloadError),
#[error("I/O Error: {0}")]
Io(#[from] io::Error),
#[error(transparent)]
TaskJoinError(#[from] task::JoinError),
#[cfg(feature = "git")]
#[error("Failed to shallow clone git repository: {0}")]
GitError(#[from] GitError),
}
impl From<CargoTomlError> for RegistryError {
fn from(e: CargoTomlError) -> Self {
Self::from(Box::new(e))
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Registry {
Sparse(Arc<SparseRegistry>),
#[cfg(feature = "git")]
Git(GitRegistry),
}
impl Default for Registry {
fn default() -> Self {
Self::crates_io_sparse_registry()
}
}
#[derive(Debug, ThisError)]
#[error("Invalid registry `{src}`, {inner}")]
pub struct InvalidRegistryError {
src: CompactString,
#[source]
inner: InvalidRegistryErrorInner,
}
#[derive(Debug, ThisError)]
enum InvalidRegistryErrorInner {
#[cfg(feature = "git")]
#[error("failed to parse git url {0}")]
GitUrlParseErr(#[from] Box<GitUrlParseError>),
#[error("failed to parse sparse registry url: {0}")]
UrlParseErr(#[from] UrlParseError),
#[error("expected protocol http(s), actual url `{0}`")]
InvalidScheme(Box<Url>),
#[cfg(not(feature = "git"))]
#[error("git registry not supported")]
GitRegistryNotSupported,
}
impl Registry {
/// Return a crates.io sparse registry
pub fn crates_io_sparse_registry() -> Self {
Self::Sparse(Arc::new(SparseRegistry::new(
Url::parse("https://index.crates.io/").unwrap(),
)))
}
fn from_str_inner(s: &str) -> Result<Self, InvalidRegistryErrorInner> {
if let Some(s) = s.strip_prefix("sparse+") {
let url = Url::parse(s)?;
let scheme = url.scheme();
if scheme != "http" && scheme != "https" {
Err(InvalidRegistryErrorInner::InvalidScheme(Box::new(url)))
} else {
Ok(Self::Sparse(Arc::new(SparseRegistry::new(url))))
}
} else {
#[cfg(not(feature = "git"))]
{
Err(InvalidRegistryErrorInner::GitRegistryNotSupported)
}
#[cfg(feature = "git")]
{
let url = GitUrl::from_str(s).map_err(Box::new)?;
Ok(Self::Git(GitRegistry::new(url)))
}
}
}
/// Fetch the latest crate with `crate_name` and with version matching
/// `version_req`.
pub async fn fetch_crate_matched(
&self,
client: Client,
crate_name: &str,
version_req: &VersionReq,
) -> Result<Manifest<Meta>, RegistryError> {
match self {
Self::Sparse(sparse_registry) => {
sparse_registry
.fetch_crate_matched(client, crate_name, version_req)
.await
}
#[cfg(feature = "git")]
Self::Git(git_registry) => {
git_registry
.fetch_crate_matched(client, crate_name, version_req)
.await
}
}
}
}
impl FromStr for Registry {
type Err = InvalidRegistryError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_str_inner(s).map_err(|inner| InvalidRegistryError {
src: s.into(),
inner,
})
}
}
#[cfg(test)]
mod test {
use std::num::NonZeroU16;
use toml_edit::ser::to_string;
use super::*;
/// Mark this as an async fn so that you won't accidentally use it in
/// sync context.
async fn create_client() -> Client {
Client::new(
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
None,
NonZeroU16::new(10).unwrap(),
1.try_into().unwrap(),
[],
)
.unwrap()
}
#[tokio::test]
async fn test_crates_io_sparse_registry() {
let client = create_client().await;
let sparse_registry: Registry = Registry::crates_io_sparse_registry();
assert!(
matches!(sparse_registry, Registry::Sparse(_)),
"{:?}",
sparse_registry
);
let crate_name = "cargo-binstall";
let version_req = &VersionReq::parse("=1.0.0").unwrap();
let manifest_from_sparse = sparse_registry
.fetch_crate_matched(client.clone(), crate_name, version_req)
.await
.unwrap();
let manifest_from_cratesio_api = fetch_crate_cratesio_api(client, crate_name, version_req)
.await
.unwrap();
let serialized_manifest_from_sparse = to_string(&manifest_from_sparse).unwrap();
let serialized_manifest_from_cratesio_api = to_string(&manifest_from_cratesio_api).unwrap();
assert_eq!(
serialized_manifest_from_sparse,
serialized_manifest_from_cratesio_api
);
}
#[cfg(feature = "git")]
#[tokio::test]
async fn test_crates_io_git_registry() {
let client = create_client().await;
let git_registry: Registry = "https://github.com/rust-lang/crates.io-index"
.parse()
.unwrap();
assert!(
matches!(git_registry, Registry::Git(_)),
"{:?}",
git_registry
);
let crate_name = "cargo-binstall";
let version_req = &VersionReq::parse("=1.0.0").unwrap();
let manifest_from_git = git_registry
.fetch_crate_matched(client.clone(), crate_name, version_req)
.await
.unwrap();
let manifest_from_cratesio_api = Registry::default()
.fetch_crate_matched(client, crate_name, version_req)
.await
.unwrap();
let serialized_manifest_from_git = to_string(&manifest_from_git).unwrap();
let serialized_manifest_from_cratesio_api = to_string(&manifest_from_cratesio_api).unwrap();
assert_eq!(
serialized_manifest_from_git,
serialized_manifest_from_cratesio_api
);
}
}