1#![allow(unknown_lints)]
2#![allow(clippy::identity_op)] use std::collections::BTreeMap;
5use std::fs::File;
6use std::io::prelude::*;
7use std::io::{Cursor, SeekFrom};
8use std::time::Instant;
9
10use curl::easy::{Easy, List};
11use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
12use serde::{Deserialize, Serialize};
13use url::Url;
14
15pub type Result<T> = std::result::Result<T, Error>;
16
17pub struct Registry {
18 host: String,
20 token: Option<String>,
23 handle: Easy,
25 auth_required: bool,
27}
28
29#[derive(PartialEq, Clone, Copy)]
30pub enum Auth {
31 Authorized,
32 Unauthorized,
33}
34
35#[derive(Deserialize)]
36pub struct Crate {
37 pub name: String,
38 pub description: Option<String>,
39 pub max_version: String,
40}
41
42#[derive(Serialize, Deserialize)]
47pub struct NewCrate {
48 pub name: String,
49 pub vers: String,
50 pub deps: Vec<NewCrateDependency>,
51 pub features: BTreeMap<String, Vec<String>>,
52 pub authors: Vec<String>,
53 pub description: Option<String>,
54 pub documentation: Option<String>,
55 pub homepage: Option<String>,
56 pub readme: Option<String>,
57 pub readme_file: Option<String>,
58 pub keywords: Vec<String>,
59 pub categories: Vec<String>,
60 pub license: Option<String>,
61 pub license_file: Option<String>,
62 pub repository: Option<String>,
63 pub badges: BTreeMap<String, BTreeMap<String, String>>,
64 pub links: Option<String>,
65 pub rust_version: Option<String>,
66}
67
68#[derive(Serialize, Deserialize)]
69pub struct NewCrateDependency {
70 pub optional: bool,
71 pub default_features: bool,
72 pub name: String,
73 pub features: Vec<String>,
74 pub version_req: String,
75 pub target: Option<String>,
76 pub kind: String,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub registry: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub explicit_name_in_toml: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub artifact: Option<Vec<String>>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub bindep_target: Option<String>,
85 #[serde(default, skip_serializing_if = "is_false")]
86 pub lib: bool,
87}
88
89fn is_false(x: &bool) -> bool {
90 *x == false
91}
92
93#[derive(Deserialize)]
94pub struct User {
95 pub id: u32,
96 pub login: String,
97 pub avatar: Option<String>,
98 pub email: Option<String>,
99 pub name: Option<String>,
100}
101
102pub struct Warnings {
103 pub invalid_categories: Vec<String>,
104 pub invalid_badges: Vec<String>,
105 pub other: Vec<String>,
106}
107
108#[derive(Deserialize)]
109struct R {
110 ok: bool,
111}
112#[derive(Deserialize)]
113struct OwnerResponse {
114 ok: bool,
115 msg: String,
116}
117#[derive(Deserialize)]
118struct ApiErrorList {
119 errors: Vec<ApiError>,
120}
121#[derive(Deserialize)]
122struct ApiError {
123 detail: String,
124}
125#[derive(Serialize)]
126struct OwnersReq<'a> {
127 users: &'a [&'a str],
128}
129#[derive(Deserialize)]
130struct Users {
131 users: Vec<User>,
132}
133#[derive(Deserialize)]
134struct TotalCrates {
135 total: u32,
136}
137#[derive(Deserialize)]
138struct Crates {
139 crates: Vec<Crate>,
140 meta: TotalCrates,
141}
142
143#[derive(Debug, thiserror::Error)]
145pub enum Error {
146 #[error(transparent)]
148 Curl(#[from] curl::Error),
149
150 #[error(transparent)]
153 Json(#[from] serde_json::Error),
154
155 #[error("failed to seek tarball")]
157 Io(#[from] std::io::Error),
158
159 #[error("invalid response body from server")]
161 Utf8(#[from] std::string::FromUtf8Error),
162
163 #[error(
165 "the remote server responded with an error{}: {}",
166 status(*code),
167 errors.join(", "),
168 )]
169 Api {
170 code: u32,
171 headers: Vec<String>,
172 errors: Vec<String>,
173 },
174
175 #[error(
177 "failed to get a 200 OK response, got {code}\nheaders:\n\t{}\nbody:\n{body}",
178 headers.join("\n\t"),
179 )]
180 Code {
181 code: u32,
182 headers: Vec<String>,
183 body: String,
184 },
185
186 #[error("{0}")]
188 InvalidToken(&'static str),
189
190 #[error(
193 "Request timed out after 30 seconds. If you're trying to \
194 upload a crate it may be too large. If the crate is under \
195 10MB in size, you can email help@crates.io for assistance.\n\
196 Total size was {0}."
197 )]
198 Timeout(u64),
199}
200
201impl Registry {
202 pub fn new_handle(
216 host: String,
217 token: Option<String>,
218 handle: Easy,
219 auth_required: bool,
220 ) -> Registry {
221 Registry {
222 host,
223 token,
224 handle,
225 auth_required,
226 }
227 }
228
229 pub fn set_token(&mut self, token: Option<String>) {
230 self.token = token;
231 }
232
233 fn token(&self) -> Result<&str> {
234 let token = self.token.as_ref().ok_or_else(|| {
235 Error::InvalidToken("no upload token found, please run `cargo login`")
236 })?;
237 check_token(token)?;
238 Ok(token)
239 }
240
241 pub fn host(&self) -> &str {
242 &self.host
243 }
244
245 pub fn host_is_crates_io(&self) -> bool {
246 is_url_crates_io(&self.host)
247 }
248
249 pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> Result<String> {
250 let body = serde_json::to_string(&OwnersReq { users: owners })?;
251 let body = self.put(&format!("/crates/{}/owners", krate), body.as_bytes())?;
252 assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
253 Ok(serde_json::from_str::<OwnerResponse>(&body)?.msg)
254 }
255
256 pub fn remove_owners(&mut self, krate: &str, owners: &[&str]) -> Result<()> {
257 let body = serde_json::to_string(&OwnersReq { users: owners })?;
258 let body = self.delete(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
259 assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
260 Ok(())
261 }
262
263 pub fn list_owners(&mut self, krate: &str) -> Result<Vec<User>> {
264 let body = self.get(&format!("/crates/{}/owners", krate))?;
265 Ok(serde_json::from_str::<Users>(&body)?.users)
266 }
267
268 pub fn get_crate_data(&mut self, krate: &str) -> Result<String> {
269 self.handle.get(true)?;
270 self.req(&format!("/crates/{}", krate), None, Auth::Unauthorized)
271 }
272
273 pub fn get_crate_dependencies(&mut self, krate: &str, version: &str) -> Result<String> {
274 self.handle.get(true)?;
279 self.req(
280 &format!("/crates/{}/{}/dependencies", krate, version),
281 None,
282 Auth::Unauthorized,
283 )
284 }
285
286 pub fn publish(&mut self, krate: &NewCrate, mut tarball: &File) -> Result<Warnings> {
287 let json = serde_json::to_string(krate)?;
288 let tarball_len = tarball.seek(SeekFrom::End(0))?;
301 tarball.seek(SeekFrom::Start(0))?;
302 let header = {
303 let mut w = Vec::new();
304 w.extend(&(json.len() as u32).to_le_bytes());
305 w.extend(json.as_bytes().iter().cloned());
306 w.extend(&(tarball_len as u32).to_le_bytes());
307 w
308 };
309 let size = tarball_len as usize + header.len();
310 let mut body = Cursor::new(header).chain(tarball);
311
312 let url = format!("{}/api/v1/crates/new", self.host);
313
314 self.handle.put(true)?;
315 self.handle.url(&url)?;
316 self.handle.in_filesize(size as u64)?;
317 let mut headers = List::new();
318 headers.append("Accept: application/json")?;
319 headers.append("User-Agent: cargo-show (https://github.com/g-k/cargo-show)")?;
320 headers.append(&format!("Authorization: {}", self.token()?))?;
321 self.handle.http_headers(headers)?;
322
323 let started = Instant::now();
324 let body = self
325 .handle(&mut |buf| body.read(buf).unwrap_or(0))
326 .map_err(|e| match e {
327 Error::Code { code, .. }
328 if code == 503
329 && started.elapsed().as_secs() >= 29
330 && self.host_is_crates_io() =>
331 {
332 Error::Timeout(tarball_len)
333 }
334 _ => e.into(),
335 })?;
336
337 let response = if body.is_empty() {
338 "{}".parse()?
339 } else {
340 body.parse::<serde_json::Value>()?
341 };
342
343 let invalid_categories: Vec<String> = response
344 .get("warnings")
345 .and_then(|j| j.get("invalid_categories"))
346 .and_then(|j| j.as_array())
347 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
348 .unwrap_or_else(Vec::new);
349
350 let invalid_badges: Vec<String> = response
351 .get("warnings")
352 .and_then(|j| j.get("invalid_badges"))
353 .and_then(|j| j.as_array())
354 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
355 .unwrap_or_else(Vec::new);
356
357 let other: Vec<String> = response
358 .get("warnings")
359 .and_then(|j| j.get("other"))
360 .and_then(|j| j.as_array())
361 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
362 .unwrap_or_else(Vec::new);
363
364 Ok(Warnings {
365 invalid_categories,
366 invalid_badges,
367 other,
368 })
369 }
370
371 pub fn search(&mut self, query: &str, limit: u32) -> Result<(Vec<Crate>, u32)> {
372 let formatted_query = percent_encode(query.as_bytes(), NON_ALPHANUMERIC);
373 let body = self.req(
374 &format!("/crates?q={}&per_page={}", formatted_query, limit),
375 None,
376 Auth::Unauthorized,
377 )?;
378
379 let crates = serde_json::from_str::<Crates>(&body)?;
380 Ok((crates.crates, crates.meta.total))
381 }
382
383 pub fn yank(&mut self, krate: &str, version: &str) -> Result<()> {
384 let body = self.delete(&format!("/crates/{}/{}/yank", krate, version), None)?;
385 assert!(serde_json::from_str::<R>(&body)?.ok);
386 Ok(())
387 }
388
389 pub fn unyank(&mut self, krate: &str, version: &str) -> Result<()> {
390 let body = self.put(&format!("/crates/{}/{}/unyank", krate, version), &[])?;
391 assert!(serde_json::from_str::<R>(&body)?.ok);
392 Ok(())
393 }
394
395 fn put(&mut self, path: &str, b: &[u8]) -> Result<String> {
396 self.handle.put(true)?;
397 self.req(path, Some(b), Auth::Authorized)
398 }
399
400 fn get(&mut self, path: &str) -> Result<String> {
401 self.handle.get(true)?;
402 self.req(path, None, Auth::Authorized)
403 }
404
405 fn delete(&mut self, path: &str, b: Option<&[u8]>) -> Result<String> {
406 self.handle.custom_request("DELETE")?;
407 self.req(path, b, Auth::Authorized)
408 }
409
410 fn req(&mut self, path: &str, body: Option<&[u8]>, authorized: Auth) -> Result<String> {
411 self.handle.url(&format!("{}/api/v1{}", self.host, path))?;
412 let mut headers = List::new();
413 headers.append("Accept: application/json")?;
414 headers.append("User-Agent: cargo-show (https://github.com/g-k/cargo-show)")?;
415 if body.is_some() {
416 headers.append("Content-Type: application/json")?;
417 }
418
419 if self.auth_required || authorized == Auth::Authorized {
420 headers.append(&format!("Authorization: {}", self.token()?))?;
421 }
422 self.handle.http_headers(headers)?;
423 match body {
424 Some(mut body) => {
425 self.handle.upload(true)?;
426 self.handle.in_filesize(body.len() as u64)?;
427 self.handle(&mut |buf| body.read(buf).unwrap_or(0))
428 .map_err(|e| e.into())
429 }
430 None => self.handle(&mut |_| 0).map_err(|e| e.into()),
431 }
432 }
433
434 fn handle(&mut self, read: &mut dyn FnMut(&mut [u8]) -> usize) -> Result<String> {
435 let mut headers = vec![String::from(
436 "User-Agent: cargo-show (https://github.com/g-k/cargo-show)",
437 )];
438 let mut body = Vec::new();
439 {
440 let mut handle = self.handle.transfer();
441 handle.read_function(|buf| Ok(read(buf)))?;
442 handle.write_function(|data| {
443 body.extend_from_slice(data);
444 Ok(data.len())
445 })?;
446 handle.header_function(|data| {
447 let s = String::from_utf8_lossy(data).trim().to_string();
450 if s.contains('\n') {
452 return true;
453 }
454 headers.push(s);
455 true
456 })?;
457 handle.perform()?;
458 }
459
460 let body = String::from_utf8(body)?;
461 let errors = serde_json::from_str::<ApiErrorList>(&body)
462 .ok()
463 .map(|s| s.errors.into_iter().map(|s| s.detail).collect::<Vec<_>>());
464
465 match (self.handle.response_code()?, errors) {
466 (0, None) => Ok(body),
467 (code, None) if is_success(code) => Ok(body),
468 (code, Some(errors)) => Err(Error::Api {
469 code,
470 headers,
471 errors,
472 }),
473 (code, None) => Err(Error::Code {
474 code,
475 headers,
476 body,
477 }),
478 }
479 }
480}
481
482fn is_success(code: u32) -> bool {
483 code >= 200 && code < 300
484}
485
486fn status(code: u32) -> String {
487 if is_success(code) {
488 String::new()
489 } else {
490 let reason = reason(code);
491 format!(" (status {code} {reason})")
492 }
493}
494
495fn reason(code: u32) -> &'static str {
496 match code {
498 100 => "Continue",
499 101 => "Switching Protocol",
500 103 => "Early Hints",
501 200 => "OK",
502 201 => "Created",
503 202 => "Accepted",
504 203 => "Non-Authoritative Information",
505 204 => "No Content",
506 205 => "Reset Content",
507 206 => "Partial Content",
508 300 => "Multiple Choice",
509 301 => "Moved Permanently",
510 302 => "Found",
511 303 => "See Other",
512 304 => "Not Modified",
513 307 => "Temporary Redirect",
514 308 => "Permanent Redirect",
515 400 => "Bad Request",
516 401 => "Unauthorized",
517 402 => "Payment Required",
518 403 => "Forbidden",
519 404 => "Not Found",
520 405 => "Method Not Allowed",
521 406 => "Not Acceptable",
522 407 => "Proxy Authentication Required",
523 408 => "Request Timeout",
524 409 => "Conflict",
525 410 => "Gone",
526 411 => "Length Required",
527 412 => "Precondition Failed",
528 413 => "Payload Too Large",
529 414 => "URI Too Long",
530 415 => "Unsupported Media Type",
531 416 => "Request Range Not Satisfiable",
532 417 => "Expectation Failed",
533 429 => "Too Many Requests",
534 431 => "Request Header Fields Too Large",
535 500 => "Internal Server Error",
536 501 => "Not Implemented",
537 502 => "Bad Gateway",
538 503 => "Service Unavailable",
539 504 => "Gateway Timeout",
540 _ => "<unknown>",
541 }
542}
543
544pub fn is_url_crates_io(url: &str) -> bool {
546 Url::parse(url)
547 .map(|u| u.host_str() == Some("crates.io"))
548 .unwrap_or(false)
549}
550
551pub fn check_token(token: &str) -> Result<()> {
557 if token.is_empty() {
558 return Err(Error::InvalidToken("please provide a non-empty token"));
559 }
560 if token.bytes().all(|b| {
561 b >= 32 && b < 127 || b == b'\t'
566 }) {
567 Ok(())
568 } else {
569 Err(Error::InvalidToken(
570 "token contains invalid characters.\nOnly printable ISO-8859-1 characters \
571 are allowed as it is sent in a HTTPS header.",
572 ))
573 }
574}