#![allow(unknown_lints)] #![allow(clippy::identity_op)] // used for vertical alignment use std::collections::BTreeMap; use std::fs::File; use std::io::prelude::*; use std::io::Cursor; use std::time::Instant; use anyhow::{bail, Result}; use curl::easy::{Easy, List}; use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use serde::{Deserialize, Serialize}; use url::Url; pub struct Registry { /// The base URL for issuing API requests. host: String, /// Optional authorization token. /// If None, commands requiring authorization will fail. token: Option, /// Curl handle for issuing requests. handle: Easy, } #[derive(PartialEq, Clone, Copy)] pub enum Auth { Authorized, Unauthorized, } #[derive(Deserialize)] pub struct Crate { pub name: String, pub description: Option, pub max_version: String, } #[derive(Serialize)] pub struct NewCrate { pub name: String, pub vers: String, pub deps: Vec, pub features: BTreeMap>, pub authors: Vec, pub description: Option, pub documentation: Option, pub homepage: Option, pub readme: Option, pub readme_file: Option, pub keywords: Vec, pub categories: Vec, pub license: Option, pub license_file: Option, pub repository: Option, pub badges: BTreeMap>, #[serde(default)] pub links: Option, } #[derive(Serialize)] pub struct NewCrateDependency { pub optional: bool, pub default_features: bool, pub name: String, pub features: Vec, pub version_req: String, pub target: Option, pub kind: String, #[serde(skip_serializing_if = "Option::is_none")] pub registry: Option, #[serde(skip_serializing_if = "Option::is_none")] pub explicit_name_in_toml: Option, } #[derive(Deserialize)] pub struct User { pub id: u32, pub login: String, pub avatar: Option, pub email: Option, pub name: Option, } pub struct Warnings { pub invalid_categories: Vec, pub invalid_badges: Vec, pub other: Vec, } #[derive(Deserialize)] struct R { ok: bool, } #[derive(Deserialize)] struct OwnerResponse { ok: bool, msg: String, } #[derive(Deserialize)] struct ApiErrorList { errors: Vec, } #[derive(Deserialize)] struct ApiError { detail: String, } #[derive(Serialize)] struct OwnersReq<'a> { users: &'a [&'a str], } #[derive(Deserialize)] struct Users { users: Vec, } #[derive(Deserialize)] struct TotalCrates { total: u32, } #[derive(Deserialize)] struct Crates { crates: Vec, meta: TotalCrates, } impl Registry { pub fn new(host: String, token: Option) -> Registry { Registry::new_handle(host, token, Easy::new()) } pub fn new_handle(host: String, token: Option, handle: Easy) -> Registry { Registry { host, token, handle, } } pub fn host(&self) -> &str { &self.host } pub fn host_is_crates_io(&self) -> bool { Url::parse(self.host()) .map(|u| u.host_str() == Some("crates.io")) .unwrap_or(false) } pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> Result { let body = serde_json::to_string(&OwnersReq { users: owners })?; let body = self.put(&format!("/crates/{}/owners", krate), body.as_bytes())?; assert!(serde_json::from_str::(&body)?.ok); Ok(serde_json::from_str::(&body)?.msg) } pub fn remove_owners(&mut self, krate: &str, owners: &[&str]) -> Result<()> { let body = serde_json::to_string(&OwnersReq { users: owners })?; let body = self.delete(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?; assert!(serde_json::from_str::(&body)?.ok); Ok(()) } pub fn list_owners(&mut self, krate: &str) -> Result> { let body = self.get(&format!("/crates/{}/owners", krate))?; Ok(serde_json::from_str::(&body)?.users) } pub fn publish(&mut self, krate: &NewCrate, tarball: &File) -> Result { let json = serde_json::to_string(krate)?; // Prepare the body. The format of the upload request is: // // // (metadata for the package) // // let stat = tarball.metadata()?; let header = { let mut w = Vec::new(); w.extend( [ (json.len() >> 0) as u8, (json.len() >> 8) as u8, (json.len() >> 16) as u8, (json.len() >> 24) as u8, ] .iter() .cloned(), ); w.extend(json.as_bytes().iter().cloned()); w.extend( [ (stat.len() >> 0) as u8, (stat.len() >> 8) as u8, (stat.len() >> 16) as u8, (stat.len() >> 24) as u8, ] .iter() .cloned(), ); w }; let size = stat.len() as usize + header.len(); let mut body = Cursor::new(header).chain(tarball); let url = format!("{}/api/v1/crates/new", self.host); let token = match self.token.as_ref() { Some(s) => s, None => bail!("no upload token found, please run `cargo login`"), }; self.handle.put(true)?; self.handle.url(&url)?; self.handle.in_filesize(size as u64)?; let mut headers = List::new(); headers.append("Accept: application/json")?; headers.append(&format!("Authorization: {}", token))?; self.handle.http_headers(headers)?; let body = self.handle(&mut |buf| body.read(buf).unwrap_or(0))?; let response = if body.is_empty() { "{}".parse()? } else { body.parse::()? }; let invalid_categories: Vec = response .get("warnings") .and_then(|j| j.get("invalid_categories")) .and_then(|j| j.as_array()) .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect()) .unwrap_or_else(Vec::new); let invalid_badges: Vec = response .get("warnings") .and_then(|j| j.get("invalid_badges")) .and_then(|j| j.as_array()) .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect()) .unwrap_or_else(Vec::new); let other: Vec = response .get("warnings") .and_then(|j| j.get("other")) .and_then(|j| j.as_array()) .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect()) .unwrap_or_else(Vec::new); Ok(Warnings { invalid_categories, invalid_badges, other, }) } pub fn search(&mut self, query: &str, limit: u32) -> Result<(Vec, u32)> { let formatted_query = percent_encode(query.as_bytes(), NON_ALPHANUMERIC); let body = self.req( &format!("/crates?q={}&per_page={}", formatted_query, limit), None, Auth::Unauthorized, )?; let crates = serde_json::from_str::(&body)?; Ok((crates.crates, crates.meta.total)) } pub fn yank(&mut self, krate: &str, version: &str) -> Result<()> { let body = self.delete(&format!("/crates/{}/{}/yank", krate, version), None)?; assert!(serde_json::from_str::(&body)?.ok); Ok(()) } pub fn unyank(&mut self, krate: &str, version: &str) -> Result<()> { let body = self.put(&format!("/crates/{}/{}/unyank", krate, version), &[])?; assert!(serde_json::from_str::(&body)?.ok); Ok(()) } fn put(&mut self, path: &str, b: &[u8]) -> Result { self.handle.put(true)?; self.req(path, Some(b), Auth::Authorized) } fn get(&mut self, path: &str) -> Result { self.handle.get(true)?; self.req(path, None, Auth::Authorized) } fn delete(&mut self, path: &str, b: Option<&[u8]>) -> Result { self.handle.custom_request("DELETE")?; self.req(path, b, Auth::Authorized) } fn req(&mut self, path: &str, body: Option<&[u8]>, authorized: Auth) -> Result { self.handle.url(&format!("{}/api/v1{}", self.host, path))?; let mut headers = List::new(); headers.append("Accept: application/json")?; headers.append("Content-Type: application/json")?; if authorized == Auth::Authorized { let token = match self.token.as_ref() { Some(s) => s, None => bail!("no upload token found, please run `cargo login`"), }; headers.append(&format!("Authorization: {}", token))?; } self.handle.http_headers(headers)?; match body { Some(mut body) => { self.handle.upload(true)?; self.handle.in_filesize(body.len() as u64)?; self.handle(&mut |buf| body.read(buf).unwrap_or(0)) } None => self.handle(&mut |_| 0), } } fn handle(&mut self, read: &mut dyn FnMut(&mut [u8]) -> usize) -> Result { let mut headers = Vec::new(); let mut body = Vec::new(); let started; { let mut handle = self.handle.transfer(); handle.read_function(|buf| Ok(read(buf)))?; handle.write_function(|data| { body.extend_from_slice(data); Ok(data.len()) })?; handle.header_function(|data| { headers.push(String::from_utf8_lossy(data).into_owned()); true })?; started = Instant::now(); handle.perform()?; } let body = match String::from_utf8(body) { Ok(body) => body, Err(..) => bail!("response body was not valid utf-8"), }; let errors = serde_json::from_str::(&body) .ok() .map(|s| s.errors.into_iter().map(|s| s.detail).collect::>()); match (self.handle.response_code()?, errors) { (0, None) | (200, None) => {} (503, None) if started.elapsed().as_secs() >= 29 && self.host_is_crates_io() => bail!( "Request timed out after 30 seconds. If you're trying to \ upload a crate it may be too large. If the crate is under \ 10MB in size, you can email help@crates.io for assistance." ), (code, Some(errors)) => { let reason = reason(code); bail!( "api errors (status {} {}): {}", code, reason, errors.join(", ") ) } (code, None) => bail!( "failed to get a 200 OK response, got {}\n\ headers:\n\ \t{}\n\ body:\n\ {}", code, headers.join("\n\t"), body, ), } Ok(body) } } fn reason(code: u32) -> &'static str { // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status match code { 100 => "Continue", 101 => "Switching Protocol", 103 => "Early Hints", 200 => "OK", 201 => "Created", 202 => "Accepted", 203 => "Non-Authoritative Information", 204 => "No Content", 205 => "Reset Content", 206 => "Partial Content", 300 => "Multiple Choice", 301 => "Moved Permanently", 302 => "Found", 303 => "See Other", 304 => "Not Modified", 307 => "Temporary Redirect", 308 => "Permanent Redirect", 400 => "Bad Request", 401 => "Unauthorized", 402 => "Payment Required", 403 => "Forbidden", 404 => "Not Found", 405 => "Method Not Allowed", 406 => "Not Acceptable", 407 => "Proxy Authentication Required", 408 => "Request Timeout", 409 => "Conflict", 410 => "Gone", 411 => "Length Required", 412 => "Precondition Failed", 413 => "Payload Too Large", 414 => "URI Too Long", 415 => "Unsupported Media Type", 416 => "Request Range Not Satisfiable", 417 => "Expectation Failed", 429 => "Too Many Requests", 431 => "Request Header Fields Too Large", 500 => "Internal Server Error", 501 => "Not Implemented", 502 => "Bad Gateway", 503 => "Service Unavailable", 504 => "Gateway Timeout", _ => "", } }