1use 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::{NON_ALPHANUMERIC, percent_encode};
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 = "std::ops::Not::not")]
86 pub lib: bool,
87}
88
89#[derive(Deserialize)]
90pub struct User {
91 pub id: u32,
92 pub login: String,
93 pub avatar: Option<String>,
94 pub email: Option<String>,
95 pub name: Option<String>,
96}
97
98pub struct Warnings {
99 pub invalid_categories: Vec<String>,
100 pub invalid_badges: Vec<String>,
101 pub other: Vec<String>,
102}
103
104#[derive(Deserialize)]
105struct R {
106 ok: bool,
107}
108#[derive(Deserialize)]
109struct OwnerResponse {
110 ok: bool,
111 msg: String,
112}
113#[derive(Deserialize)]
114struct ApiErrorList {
115 errors: Vec<ApiError>,
116}
117#[derive(Deserialize)]
118struct ApiError {
119 detail: String,
120}
121#[derive(Serialize)]
122struct OwnersReq<'a> {
123 users: &'a [&'a str],
124}
125#[derive(Deserialize)]
126struct Users {
127 users: Vec<User>,
128}
129#[derive(Deserialize)]
130struct TotalCrates {
131 total: u32,
132}
133#[derive(Deserialize)]
134struct Crates {
135 crates: Vec<Crate>,
136 meta: TotalCrates,
137}
138
139#[derive(Debug, thiserror::Error)]
141pub enum Error {
142 #[error(transparent)]
144 Curl(#[from] curl::Error),
145
146 #[error(transparent)]
149 Json(#[from] serde_json::Error),
150
151 #[error("failed to seek tarball")]
153 Io(#[from] std::io::Error),
154
155 #[error("invalid response body from server")]
157 Utf8(#[from] std::string::FromUtf8Error),
158
159 #[error(
161 "the remote server responded with an error{}: {}",
162 status(*code),
163 errors.join(", "),
164 )]
165 Api {
166 code: u32,
167 headers: Vec<String>,
168 errors: Vec<String>,
169 },
170
171 #[error(
173 "failed to get a 200 OK response, got {code}\nheaders:\n\t{}\nbody:\n{body}",
174 headers.join("\n\t"),
175 )]
176 Code {
177 code: u32,
178 headers: Vec<String>,
179 body: String,
180 },
181
182 #[error("{0}")]
184 InvalidToken(&'static str),
185
186 #[error(
189 "Request timed out after 30 seconds. If you're trying to \
190 upload a crate it may be too large. If the crate is under \
191 10MB in size, you can email help@crates.io for assistance.\n\
192 Total size was {0}."
193 )]
194 Timeout(u64),
195}
196
197impl Registry {
198 pub fn new_handle(
212 host: String,
213 token: Option<String>,
214 handle: Easy,
215 auth_required: bool,
216 ) -> Registry {
217 Registry {
218 host,
219 token,
220 handle,
221 auth_required,
222 }
223 }
224
225 pub fn set_token(&mut self, token: Option<String>) {
226 self.token = token;
227 }
228
229 fn token(&self) -> Result<&str> {
230 let token = self.token.as_ref().ok_or_else(|| {
231 Error::InvalidToken("no upload token found, please run `cargo login`")
232 })?;
233 check_token(token)?;
234 Ok(token)
235 }
236
237 pub fn host(&self) -> &str {
238 &self.host
239 }
240
241 pub fn host_is_crates_io(&self) -> bool {
242 is_url_crates_io(&self.host)
243 }
244
245 pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> Result<String> {
246 let body = serde_json::to_string(&OwnersReq { users: owners })?;
247 let body = self.put(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
248 assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
249 Ok(serde_json::from_str::<OwnerResponse>(&body)?.msg)
250 }
251
252 pub fn remove_owners(&mut self, krate: &str, owners: &[&str]) -> Result<()> {
253 let body = serde_json::to_string(&OwnersReq { users: owners })?;
254 let body = self.delete(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
255 assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
256 Ok(())
257 }
258
259 pub fn list_owners(&mut self, krate: &str) -> Result<Vec<User>> {
260 let body = self.get(&format!("/crates/{}/owners", krate))?;
261 Ok(serde_json::from_str::<Users>(&body)?.users)
262 }
263
264 pub fn publish(&mut self, krate: &NewCrate, mut tarball: &File) -> Result<Warnings> {
265 let json = serde_json::to_string(krate)?;
266 let tarball_len = tarball.seek(SeekFrom::End(0))?;
279 tarball.seek(SeekFrom::Start(0))?;
280 let header = {
281 let mut w = Vec::new();
282 w.extend(&(json.len() as u32).to_le_bytes());
283 w.extend(json.as_bytes().iter().cloned());
284 w.extend(&(tarball_len as u32).to_le_bytes());
285 w
286 };
287 let size = tarball_len as usize + header.len();
288 let mut body = Cursor::new(header).chain(tarball);
289
290 let url = format!("{}/api/v1/crates/new", self.host);
291
292 self.handle.put(true)?;
293 self.handle.url(&url)?;
294 self.handle.in_filesize(size as u64)?;
295 let mut headers = List::new();
296 headers.append("Content-Type: application/octet-stream")?;
297 headers.append("Accept: application/json")?;
298 headers.append(&format!("Authorization: {}", self.token()?))?;
299 self.handle.http_headers(headers)?;
300
301 let started = Instant::now();
302 let body = self
303 .handle(&mut |buf| body.read(buf).unwrap_or(0))
304 .map_err(|e| match e {
305 Error::Code { code, .. }
306 if code == 503
307 && started.elapsed().as_secs() >= 29
308 && self.host_is_crates_io() =>
309 {
310 Error::Timeout(tarball_len)
311 }
312 _ => e.into(),
313 })?;
314
315 let response = if body.is_empty() {
316 "{}".parse()?
317 } else {
318 body.parse::<serde_json::Value>()?
319 };
320
321 let invalid_categories: Vec<String> = response
322 .get("warnings")
323 .and_then(|j| j.get("invalid_categories"))
324 .and_then(|j| j.as_array())
325 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
326 .unwrap_or_else(Vec::new);
327
328 let invalid_badges: Vec<String> = response
329 .get("warnings")
330 .and_then(|j| j.get("invalid_badges"))
331 .and_then(|j| j.as_array())
332 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
333 .unwrap_or_else(Vec::new);
334
335 let other: Vec<String> = response
336 .get("warnings")
337 .and_then(|j| j.get("other"))
338 .and_then(|j| j.as_array())
339 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
340 .unwrap_or_else(Vec::new);
341
342 Ok(Warnings {
343 invalid_categories,
344 invalid_badges,
345 other,
346 })
347 }
348
349 pub fn search(&mut self, query: &str, limit: u32) -> Result<(Vec<Crate>, u32)> {
350 let formatted_query = percent_encode(query.as_bytes(), NON_ALPHANUMERIC);
351 let body = self.req(
352 &format!("/crates?q={}&per_page={}", formatted_query, limit),
353 None,
354 Auth::Unauthorized,
355 )?;
356
357 let crates = serde_json::from_str::<Crates>(&body)?;
358 Ok((crates.crates, crates.meta.total))
359 }
360
361 pub fn yank(&mut self, krate: &str, version: &str) -> Result<()> {
362 let body = self.delete(&format!("/crates/{}/{}/yank", krate, version), None)?;
363 assert!(serde_json::from_str::<R>(&body)?.ok);
364 Ok(())
365 }
366
367 pub fn unyank(&mut self, krate: &str, version: &str) -> Result<()> {
368 let body = self.put(&format!("/crates/{}/{}/unyank", krate, version), None)?;
369 assert!(serde_json::from_str::<R>(&body)?.ok);
370 Ok(())
371 }
372
373 fn put(&mut self, path: &str, b: Option<&[u8]>) -> Result<String> {
374 self.handle.put(true)?;
375 self.req(path, b, Auth::Authorized)
376 }
377
378 fn get(&mut self, path: &str) -> Result<String> {
379 self.handle.get(true)?;
380 self.req(path, None, Auth::Authorized)
381 }
382
383 fn delete(&mut self, path: &str, b: Option<&[u8]>) -> Result<String> {
384 self.handle.custom_request("DELETE")?;
385 self.req(path, b, Auth::Authorized)
386 }
387
388 fn req(&mut self, path: &str, body: Option<&[u8]>, authorized: Auth) -> Result<String> {
389 self.handle.url(&format!("{}/api/v1{}", self.host, path))?;
390 let mut headers = List::new();
391 headers.append("Accept: application/json")?;
392 if body.is_some() {
393 headers.append("Content-Type: application/json")?;
394 }
395
396 if self.auth_required || authorized == Auth::Authorized {
397 headers.append(&format!("Authorization: {}", self.token()?))?;
398 }
399 self.handle.http_headers(headers)?;
400 match body {
401 Some(mut body) => {
402 self.handle.upload(true)?;
403 self.handle.in_filesize(body.len() as u64)?;
404 self.handle(&mut |buf| body.read(buf).unwrap_or(0))
405 .map_err(|e| e.into())
406 }
407 None => self.handle(&mut |_| 0).map_err(|e| e.into()),
408 }
409 }
410
411 fn handle(&mut self, read: &mut dyn FnMut(&mut [u8]) -> usize) -> Result<String> {
412 let mut headers = Vec::new();
413 let mut body = Vec::new();
414 {
415 let mut handle = self.handle.transfer();
416 handle.read_function(|buf| Ok(read(buf)))?;
417 handle.write_function(|data| {
418 body.extend_from_slice(data);
419 Ok(data.len())
420 })?;
421 handle.header_function(|data| {
422 let s = String::from_utf8_lossy(data).trim().to_string();
425 if s.contains('\n') {
427 return true;
428 }
429 headers.push(s);
430 true
431 })?;
432 handle.perform()?;
433 }
434
435 let body = String::from_utf8(body)?;
436 let errors = serde_json::from_str::<ApiErrorList>(&body)
437 .ok()
438 .map(|s| s.errors.into_iter().map(|s| s.detail).collect::<Vec<_>>());
439
440 match (self.handle.response_code()?, errors) {
441 (0, None) => Ok(body),
442 (code, None) if is_success(code) => Ok(body),
443 (code, Some(errors)) => Err(Error::Api {
444 code,
445 headers,
446 errors,
447 }),
448 (code, None) => Err(Error::Code {
449 code,
450 headers,
451 body,
452 }),
453 }
454 }
455}
456
457fn is_success(code: u32) -> bool {
458 code >= 200 && code < 300
459}
460
461fn status(code: u32) -> String {
462 if is_success(code) {
463 String::new()
464 } else {
465 let reason = reason(code);
466 format!(" (status {code} {reason})")
467 }
468}
469
470fn reason(code: u32) -> &'static str {
471 match code {
473 100 => "Continue",
474 101 => "Switching Protocol",
475 103 => "Early Hints",
476 200 => "OK",
477 201 => "Created",
478 202 => "Accepted",
479 203 => "Non-Authoritative Information",
480 204 => "No Content",
481 205 => "Reset Content",
482 206 => "Partial Content",
483 300 => "Multiple Choice",
484 301 => "Moved Permanently",
485 302 => "Found",
486 303 => "See Other",
487 304 => "Not Modified",
488 307 => "Temporary Redirect",
489 308 => "Permanent Redirect",
490 400 => "Bad Request",
491 401 => "Unauthorized",
492 402 => "Payment Required",
493 403 => "Forbidden",
494 404 => "Not Found",
495 405 => "Method Not Allowed",
496 406 => "Not Acceptable",
497 407 => "Proxy Authentication Required",
498 408 => "Request Timeout",
499 409 => "Conflict",
500 410 => "Gone",
501 411 => "Length Required",
502 412 => "Precondition Failed",
503 413 => "Payload Too Large",
504 414 => "URI Too Long",
505 415 => "Unsupported Media Type",
506 416 => "Request Range Not Satisfiable",
507 417 => "Expectation Failed",
508 429 => "Too Many Requests",
509 431 => "Request Header Fields Too Large",
510 500 => "Internal Server Error",
511 501 => "Not Implemented",
512 502 => "Bad Gateway",
513 503 => "Service Unavailable",
514 504 => "Gateway Timeout",
515 _ => "<unknown>",
516 }
517}
518
519pub fn is_url_crates_io(url: &str) -> bool {
521 Url::parse(url)
522 .map(|u| u.host_str() == Some("crates.io"))
523 .unwrap_or(false)
524}
525
526pub fn check_token(token: &str) -> Result<()> {
532 if token.is_empty() {
533 return Err(Error::InvalidToken("please provide a non-empty token"));
534 }
535 if token.bytes().all(|b| {
536 b >= 32 && b < 127 || b == b'\t'
541 }) {
542 Ok(())
543 } else {
544 Err(Error::InvalidToken(
545 "token contains invalid characters.\nOnly printable ISO-8859-1 characters \
546 are allowed as it is sent in a HTTPS header.",
547 ))
548 }
549}