use std::{ borrow::Cow, path::Path, sync::{Arc, Mutex, OnceLock}, }; use binstalk_downloader::remote::Method; use binstalk_types::cargo_toml_binstall::{PkgFmt, PkgMeta, PkgSigning, Strategy}; use tokio::sync::OnceCell; use tracing::{error, info, trace}; use url::Url; use crate::{ common::*, Data, FetchError, SignaturePolicy, SignatureVerifier, SigningAlgorithm, TargetDataErased, }; const BASE_URL: &str = "https://github.com/cargo-bins/cargo-quickinstall/releases/download"; pub const QUICKINSTALL_STATS_URL: &str = "https://cargo-quickinstall-stats-server.fly.dev/record-install"; const QUICKINSTALL_SIGN_KEY: Cow<'static, str> = Cow::Borrowed("RWTdnnab2pAka9OdwgCMYyOE66M/BlQoFWaJ/JjwcPV+f3n24IRTj97t"); const QUICKINSTALL_SUPPORTED_TARGETS_URL: &str = "https://raw.githubusercontent.com/cargo-bins/cargo-quickinstall/main/supported-targets"; fn is_universal_macos(target: &str) -> bool { ["universal-apple-darwin", "universal2-apple-darwin"].contains(&target) } async fn get_quickinstall_supported_targets( client: &Client, ) -> Result<&'static [CompactString], FetchError> { static SUPPORTED_TARGETS: OnceCell> = OnceCell::const_new(); SUPPORTED_TARGETS .get_or_try_init(|| async { let bytes = client .get(Url::parse(QUICKINSTALL_SUPPORTED_TARGETS_URL)?) .send(true) .await? .bytes() .await?; let mut v: Vec = String::from_utf8_lossy(&bytes) .split_whitespace() .map(CompactString::new) .collect(); v.sort_unstable(); v.dedup(); Ok(v.into()) }) .await .map(Box::as_ref) } pub struct QuickInstall { client: Client, gh_api_client: GhApiClient, is_supported_v: OnceCell, data: Arc, package: String, package_url: Url, signature_url: Url, signature_policy: SignaturePolicy, target_data: Arc, signature_verifier: OnceLock, status: Mutex, } #[derive(Debug, Clone, Copy)] enum Status { Start, NotFound, Found, AttemptingInstall, InvalidSignature, InstalledFromTarball, } impl Status { fn as_str(&self) -> &'static str { match self { Status::Start => "start", Status::NotFound => "not-found", Status::Found => "found", Status::AttemptingInstall => "attempting-install", Status::InvalidSignature => "invalid-signature", Status::InstalledFromTarball => "installed-from-tarball", } } } impl QuickInstall { async fn is_supported(&self) -> Result { self.is_supported_v .get_or_try_init(|| async { Ok(get_quickinstall_supported_targets(&self.client) .await? .binary_search(&CompactString::new(&self.target_data.target)) .is_ok()) }) .await .copied() } fn download_signature( self: Arc, ) -> AutoAbortJoinHandle> { AutoAbortJoinHandle::spawn(async move { if self.signature_policy == SignaturePolicy::Ignore { Ok(SignatureVerifier::Noop) } else { debug!(url=%self.signature_url, "Downloading signature"); match Download::new(self.client.clone(), self.signature_url.clone()) .into_bytes() .await { Ok(signature) => { trace!(?signature, "got signature contents"); let config = PkgSigning { algorithm: SigningAlgorithm::Minisign, pubkey: QUICKINSTALL_SIGN_KEY, file: None, }; SignatureVerifier::new(&config, &signature) } Err(err) => { if self.signature_policy == SignaturePolicy::Require { error!("Failed to download signature: {err}"); Err(FetchError::MissingSignature) } else { debug!("Failed to download signature, skipping verification: {err}"); Ok(SignatureVerifier::Noop) } } } } }) } fn get_status(&self) -> Status { *self.status.lock().unwrap() } fn set_status(&self, status: Status) { *self.status.lock().unwrap() = status; } } #[async_trait::async_trait] impl super::Fetcher for QuickInstall { fn new( client: Client, gh_api_client: GhApiClient, data: Arc, target_data: Arc, signature_policy: SignaturePolicy, ) -> Arc { let crate_name = &data.name; let version = &data.version; let target = &target_data.target; let package = format!("{crate_name}-{version}-{target}"); let url = format!("{BASE_URL}/{crate_name}-{version}/{package}.tar.gz"); Arc::new(Self { client, data, gh_api_client, is_supported_v: OnceCell::new(), package_url: Url::parse(&url) .expect("package_url is pre-generated and should never be invalid url"), signature_url: Url::parse(&format!("{url}.sig")) .expect("signature_url is pre-generated and should never be invalid url"), package, signature_policy, target_data, signature_verifier: OnceLock::new(), status: Mutex::new(Status::Start), }) } fn find(self: Arc) -> JoinHandle> { tokio::spawn(async move { if !self.is_supported().await? { return Ok(false); } let download_signature_task = self.clone().download_signature(); let is_found = does_url_exist( self.client.clone(), self.gh_api_client.clone(), &self.package_url, ) .await?; if !is_found { self.set_status(Status::NotFound); return Ok(false); } if self .signature_verifier .set(download_signature_task.flattened_join().await?) .is_err() { panic!("::find is run twice"); } self.set_status(Status::Found); Ok(true) }) } fn report_to_upstream(self: Arc) { if cfg!(debug_assertions) { debug!("Not sending quickinstall report in debug mode"); } else if is_universal_macos(&self.target_data.target) { debug!( r#"Not sending quickinstall report for universal-apple-darwin and universal2-apple-darwin. Quickinstall does not support these targets, it only supports targets supported by rust officially."#, ); } else if self.is_supported_v.get().copied() != Some(false) { tokio::spawn(async move { if let Err(err) = self.report().await { warn!( "Failed to send quickinstall report for package {} (NOTE that this does not affect package resolution): {err}", self.package ) } }); } } async fn fetch_and_extract(&self, dst: &Path) -> Result { self.set_status(Status::AttemptingInstall); let Some(verifier) = self.signature_verifier.get() else { panic!("::find has not been called yet!") }; debug!(url=%self.package_url, "Downloading package"); let mut data_verifier = verifier.data_verifier()?; let files = Download::new_with_data_verifier( self.client.clone(), self.package_url.clone(), data_verifier.as_mut(), ) .and_extract(self.pkg_fmt(), dst) .await?; trace!("validating signature (if any)"); if data_verifier.validate() { if let Some(info) = verifier.info() { info!("Verified signature for package '{}': {info}", self.package); } self.set_status(Status::InstalledFromTarball); Ok(files) } else { self.set_status(Status::InvalidSignature); Err(FetchError::InvalidSignature) } } fn pkg_fmt(&self) -> PkgFmt { PkgFmt::Tgz } fn target_meta(&self) -> PkgMeta { let mut meta = self.target_data.meta.clone(); meta.pkg_fmt = Some(self.pkg_fmt()); meta.bin_dir = Some("{ bin }{ binary-ext }".to_string()); meta } fn source_name(&self) -> CompactString { CompactString::from("QuickInstall") } fn fetcher_name(&self) -> &'static str { "QuickInstall" } fn strategy(&self) -> Strategy { Strategy::QuickInstall } fn is_third_party(&self) -> bool { true } fn target(&self) -> &str { &self.target_data.target } fn target_data(&self) -> &Arc { &self.target_data } } impl QuickInstall { pub async fn report(&self) -> Result<(), FetchError> { if !self.is_supported().await? { debug!( "Not sending quickinstall report for {} since Quickinstall does not support these targets.", self.target_data.target ); return Ok(()); } let mut url = Url::parse(QUICKINSTALL_STATS_URL) .expect("stats_url is pre-generated and should never be invalid url"); url.query_pairs_mut() .append_pair("crate", &self.data.name) .append_pair("version", &self.data.version) .append_pair("target", &self.target_data.target) .append_pair( "agent", concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), ) .append_pair("status", self.get_status().as_str()); debug!("Sending installation report to quickinstall ({url})"); self.client.request(Method::POST, url).send(true).await?; Ok(()) } } #[cfg(test)] mod test { use super::{get_quickinstall_supported_targets, Client, CompactString}; use std::num::NonZeroU16; /// 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_get_quickinstall_supported_targets() { let supported_targets = get_quickinstall_supported_targets(&create_client().await) .await .unwrap(); [ "x86_64-pc-windows-msvc", "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "aarch64-pc-windows-msvc", "armv7-unknown-linux-musleabihf", "armv7-unknown-linux-gnueabihf", ] .into_iter() .for_each(|known_supported_target| { supported_targets .binary_search(&CompactString::new(known_supported_target)) .unwrap(); }); } }