1use std::collections::BTreeMap;
5use std::fs::File;
6use std::io::prelude::*;
7use std::io::{Cursor, SeekFrom};
8use std::time::Instant;
9
10use http::{Method, Request, Response, StatusCode};
11use percent_encoding::{NON_ALPHANUMERIC, percent_encode};
12use serde::{Deserialize, Serialize};
13use url::Url;
14
15type RegistryResult<T, E> = Result<T, Error<E>>;
16
17pub trait HttpClient {
22 type Error: std::error::Error + Send + Sync;
23 fn request(&self, req: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, Self::Error>;
24}
25
26pub struct Registry<T: HttpClient> {
27 host: String,
29 token: Option<String>,
32 handle: T,
34 auth_required: bool,
36}
37
38#[derive(PartialEq, Clone, Copy)]
39pub enum Auth {
40 Authorized,
41 Unauthorized,
42}
43
44#[derive(Deserialize)]
45pub struct Crate {
46 pub name: String,
47 pub description: Option<String>,
48 pub max_version: String,
49}
50
51#[derive(Serialize, Deserialize)]
56pub struct NewCrate {
57 pub name: String,
58 pub vers: String,
59 pub deps: Vec<NewCrateDependency>,
60 pub features: BTreeMap<String, Vec<String>>,
61 pub authors: Vec<String>,
62 pub description: Option<String>,
63 pub documentation: Option<String>,
64 pub homepage: Option<String>,
65 pub readme: Option<String>,
66 pub readme_file: Option<String>,
67 pub keywords: Vec<String>,
68 pub categories: Vec<String>,
69 pub license: Option<String>,
70 pub license_file: Option<String>,
71 pub repository: Option<String>,
72 pub badges: BTreeMap<String, BTreeMap<String, String>>,
73 pub links: Option<String>,
74 pub rust_version: Option<String>,
75}
76
77#[derive(Serialize, Deserialize)]
78pub struct NewCrateDependency {
79 pub optional: bool,
80 pub default_features: bool,
81 pub name: String,
82 pub features: Vec<String>,
83 pub version_req: String,
84 pub target: Option<String>,
85 pub kind: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub registry: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub explicit_name_in_toml: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub artifact: Option<Vec<String>>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub bindep_target: Option<String>,
94 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
95 pub lib: bool,
96}
97
98#[derive(Deserialize)]
99pub struct User {
100 pub id: u32,
101 pub login: String,
102 pub avatar: Option<String>,
103 pub email: Option<String>,
104 pub name: Option<String>,
105}
106
107pub struct Warnings {
108 pub invalid_categories: Vec<String>,
109 pub invalid_badges: Vec<String>,
110 pub other: Vec<String>,
111}
112
113#[derive(Deserialize)]
114struct R {
115 ok: bool,
116}
117#[derive(Deserialize)]
118struct OwnerResponse {
119 ok: bool,
120 msg: String,
121}
122#[derive(Deserialize)]
123struct ApiErrorList {
124 errors: Vec<ApiError>,
125}
126#[derive(Deserialize)]
127struct ApiError {
128 detail: String,
129}
130#[derive(Serialize)]
131struct OwnersReq<'a> {
132 users: &'a [&'a str],
133}
134#[derive(Deserialize)]
135struct Users {
136 users: Vec<User>,
137}
138#[derive(Deserialize)]
139struct TotalCrates {
140 total: u32,
141}
142#[derive(Deserialize)]
143struct Crates {
144 crates: Vec<Crate>,
145 meta: TotalCrates,
146}
147
148#[derive(Debug, thiserror::Error)]
150#[non_exhaustive]
151pub enum Error<T> {
152 #[error(transparent)]
154 Transport(T),
155
156 #[error(transparent)]
158 Http(#[from] http::Error),
159
160 #[error(transparent)]
163 Json(#[from] serde_json::Error),
164
165 #[error("failed to seek tarball")]
167 Io(#[from] std::io::Error),
168
169 #[error("invalid response body from server")]
171 Utf8(#[from] std::string::FromUtf8Error),
172
173 #[error(
175 "the remote server responded with an error{}: {}",
176 status(*code),
177 errors.join(", "),
178 )]
179 Api {
180 code: StatusCode,
181 headers: Vec<String>,
182 errors: Vec<String>,
183 },
184
185 #[error(
187 "failed to get a 200 OK response, got {}\nheaders:\n\t{}\nbody:\n{body}",
188 code.as_u16(),
189 headers.join("\n\t"),
190 )]
191 Code {
192 code: StatusCode,
193 headers: Vec<String>,
194 body: String,
195 },
196
197 #[error(transparent)]
198 InvalidToken(#[from] TokenError),
199
200 #[error(
203 "Request timed out after 30 seconds. If you're trying to \
204 upload a crate it may be too large. If the crate is under \
205 10MB in size, you can email help@crates.io for assistance.\n\
206 Total size was {0}."
207 )]
208 Timeout(u64),
209}
210
211impl<T: HttpClient> Registry<T> {
212 pub fn new_handle(host: String, token: Option<String>, handle: T, auth_required: bool) -> Self {
232 Self {
233 host,
234 token,
235 handle,
236 auth_required,
237 }
238 }
239
240 pub fn set_token(&mut self, token: Option<String>) {
241 self.token = token;
242 }
243
244 fn token(&self) -> RegistryResult<&str, T::Error> {
245 let token = self.token.as_ref().ok_or_else(|| TokenError::Missing)?;
246 check_token(token)?;
247 Ok(token)
248 }
249
250 pub fn host(&self) -> &str {
251 &self.host
252 }
253
254 pub fn host_is_crates_io(&self) -> bool {
255 is_url_crates_io(&self.host)
256 }
257
258 pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> RegistryResult<String, T::Error> {
259 let body = serde_json::to_string(&OwnersReq { users: owners })?;
260 let body = self.put(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
261 assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
262 Ok(serde_json::from_str::<OwnerResponse>(&body)?.msg)
263 }
264
265 pub fn remove_owners(&mut self, krate: &str, owners: &[&str]) -> RegistryResult<(), T::Error> {
266 let body = serde_json::to_string(&OwnersReq { users: owners })?;
267 let body = self.delete(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
268 assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
269 Ok(())
270 }
271
272 pub fn list_owners(&mut self, krate: &str) -> RegistryResult<Vec<User>, T::Error> {
273 let body = self.get(&format!("/crates/{}/owners", krate))?;
274 Ok(serde_json::from_str::<Users>(&body)?.users)
275 }
276
277 pub fn publish(
278 &mut self,
279 krate: &NewCrate,
280 mut tarball: &File,
281 ) -> RegistryResult<Warnings, T::Error> {
282 let json = serde_json::to_string(krate)?;
283 let tarball_len = tarball.seek(SeekFrom::End(0))?;
296 tarball.seek(SeekFrom::Start(0))?;
297 let header = {
298 let mut w = Vec::new();
299 w.extend(&(json.len() as u32).to_le_bytes());
300 w.extend(json.as_bytes().iter().cloned());
301 w.extend(&(tarball_len as u32).to_le_bytes());
302 w
303 };
304 let mut body = Vec::new();
305 Cursor::new(header).chain(tarball).read_to_end(&mut body)?;
306 let url = self.api_url("/crates/new");
307
308 let request = http::Request::put(url)
309 .header(http::header::CONTENT_TYPE, "application/octet-stream")
310 .header(http::header::ACCEPT, "application/json")
311 .header(http::header::AUTHORIZATION, self.token()?)
312 .body(body)?;
313 let started = Instant::now();
314 let response = self.handle.request(request).map_err(Error::Transport)?;
315 let body = self.handle(response).map_err(|e| match e {
316 Error::Code { code, .. }
317 if code == StatusCode::SERVICE_UNAVAILABLE
318 && started.elapsed().as_secs() >= 29
319 && self.host_is_crates_io() =>
320 {
321 Error::Timeout(tarball_len)
322 }
323 _ => e.into(),
324 })?;
325
326 let response = if body.is_empty() {
327 "{}".parse()?
328 } else {
329 body.parse::<serde_json::Value>()?
330 };
331
332 let invalid_categories: Vec<String> = response
333 .get("warnings")
334 .and_then(|j| j.get("invalid_categories"))
335 .and_then(|j| j.as_array())
336 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
337 .unwrap_or_else(Vec::new);
338
339 let invalid_badges: Vec<String> = response
340 .get("warnings")
341 .and_then(|j| j.get("invalid_badges"))
342 .and_then(|j| j.as_array())
343 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
344 .unwrap_or_else(Vec::new);
345
346 let other: Vec<String> = response
347 .get("warnings")
348 .and_then(|j| j.get("other"))
349 .and_then(|j| j.as_array())
350 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
351 .unwrap_or_else(Vec::new);
352
353 Ok(Warnings {
354 invalid_categories,
355 invalid_badges,
356 other,
357 })
358 }
359
360 pub fn search(
361 &mut self,
362 query: &str,
363 limit: u32,
364 ) -> RegistryResult<(Vec<Crate>, u32), T::Error> {
365 let formatted_query = percent_encode(query.as_bytes(), NON_ALPHANUMERIC);
366 let body = self.req(
367 Method::GET,
368 &format!("/crates?q={}&per_page={}", formatted_query, limit),
369 None,
370 Auth::Unauthorized,
371 )?;
372
373 let crates = serde_json::from_str::<Crates>(&body)?;
374 Ok((crates.crates, crates.meta.total))
375 }
376
377 pub fn yank(&mut self, krate: &str, version: &str) -> RegistryResult<(), T::Error> {
378 let body = self.delete(&format!("/crates/{}/{}/yank", krate, version), None)?;
379 assert!(serde_json::from_str::<R>(&body)?.ok);
380 Ok(())
381 }
382
383 pub fn unyank(&mut self, krate: &str, version: &str) -> RegistryResult<(), T::Error> {
384 let body = self.put(&format!("/crates/{}/{}/unyank", krate, version), None)?;
385 assert!(serde_json::from_str::<R>(&body)?.ok);
386 Ok(())
387 }
388
389 fn put(&mut self, path: &str, b: Option<&[u8]>) -> RegistryResult<String, T::Error> {
390 self.req(Method::PUT, path, b, Auth::Authorized)
391 }
392
393 fn get(&mut self, path: &str) -> RegistryResult<String, T::Error> {
394 self.req(Method::GET, path, None, Auth::Authorized)
395 }
396
397 fn delete(&mut self, path: &str, b: Option<&[u8]>) -> RegistryResult<String, T::Error> {
398 self.req(Method::DELETE, path, b, Auth::Authorized)
399 }
400
401 fn api_url(&self, path: &str) -> String {
402 let host = &self.host;
405 if let Some(file_url) = host.strip_prefix("file:///") {
406 format!("file://localhost/{file_url}/api/v1{path}")
407 } else {
408 format!("{host}/api/v1{path}")
409 }
410 }
411
412 fn req(
413 &mut self,
414 method: Method,
415 path: &str,
416 body: Option<&[u8]>,
417 authorized: Auth,
418 ) -> RegistryResult<String, T::Error> {
419 let url = self.api_url(path);
420 let mut request = http::Request::builder()
421 .method(method)
422 .uri(url)
423 .header(http::header::ACCEPT, "application/json");
424 if body.is_some() {
425 request = request.header(http::header::CONTENT_TYPE, "application/json");
426 }
427
428 if self.auth_required || authorized == Auth::Authorized {
429 request = request.header(http::header::AUTHORIZATION, self.token()?);
430 }
431 let request = request.body(body.unwrap_or_default().to_vec())?;
432 let response = self.handle.request(request).map_err(Error::Transport)?;
433 self.handle(response)
434 }
435
436 fn handle(&mut self, response: http::Response<Vec<u8>>) -> RegistryResult<String, T::Error> {
437 let (head, body) = response.into_parts();
438 let body = String::from_utf8(body)?;
439 let errors = serde_json::from_str::<ApiErrorList>(&body)
440 .ok()
441 .map(|s| s.errors.into_iter().map(|s| s.detail).collect::<Vec<_>>());
442
443 let headers = head
444 .headers
445 .iter()
446 .filter_map(|(k, v)| Some((k, v.to_str().ok()?)))
447 .map(|(k, v)| format!("{k}: {v}"))
448 .collect();
449
450 match (head.status, errors) {
451 (code, None) if code.is_success() => Ok(body),
452 (code, Some(errors)) => Err(Error::Api {
453 code,
454 headers,
455 errors,
456 }),
457 (code, None) => Err(Error::Code {
458 code,
459 headers,
460 body,
461 }),
462 }
463 }
464}
465
466fn status(code: StatusCode) -> String {
467 if code.is_success() {
468 String::new()
469 } else {
470 format!(" (status {code})")
471 }
472}
473
474pub fn is_url_crates_io(url: &str) -> bool {
476 Url::parse(url)
477 .map(|u| u.host_str() == Some("crates.io"))
478 .unwrap_or(false)
479}
480
481#[derive(Debug, thiserror::Error)]
482pub enum TokenError {
483 #[error("no upload token found, please run `cargo login`")]
484 Missing,
485
486 #[error("please provide a non-empty token")]
487 Empty,
488
489 #[error(
490 "token contains invalid characters.\nOnly printable ISO-8859-1 characters \
491 are allowed as it is sent in a HTTPS header."
492 )]
493 InvalidCharacters,
494}
495
496pub fn check_token(token: &str) -> Result<(), TokenError> {
502 if token.is_empty() {
503 return Err(TokenError::Empty);
504 }
505 if token.bytes().all(|b| {
506 b >= 32 && b < 127 || b == b'\t'
511 }) {
512 Ok(())
513 } else {
514 Err(TokenError::InvalidCharacters)
515 }
516}