crates_io/
lib.rs

1#![allow(unknown_lints)]
2#![allow(clippy::identity_op)] // used for vertical alignment
3
4use std::collections::BTreeMap;
5use std::fs::File;
6use std::io::prelude::*;
7use std::io::Cursor;
8
9use curl::easy::{Easy, List};
10use failure::bail;
11use serde::{Deserialize, Serialize};
12use serde_json;
13use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET};
14
15pub type Result<T> = std::result::Result<T, failure::Error>;
16
17pub struct Registry {
18    /// The base URL for issuing API requests.
19    host: String,
20    /// Optional authorization token.
21    /// If None, commands requiring authorization will fail.
22    token: Option<String>,
23    /// Curl handle for issuing requests.
24    handle: Easy,
25}
26
27#[derive(PartialEq, Clone, Copy)]
28pub enum Auth {
29    Authorized,
30    Unauthorized,
31}
32
33#[derive(Deserialize)]
34pub struct Crate {
35    pub name: String,
36    pub description: Option<String>,
37    pub max_version: String,
38}
39
40#[derive(Serialize)]
41pub struct NewCrate {
42    pub name: String,
43    pub vers: String,
44    pub deps: Vec<NewCrateDependency>,
45    pub features: BTreeMap<String, Vec<String>>,
46    pub authors: Vec<String>,
47    pub description: Option<String>,
48    pub documentation: Option<String>,
49    pub homepage: Option<String>,
50    pub readme: Option<String>,
51    pub readme_file: Option<String>,
52    pub keywords: Vec<String>,
53    pub categories: Vec<String>,
54    pub license: Option<String>,
55    pub license_file: Option<String>,
56    pub repository: Option<String>,
57    pub badges: BTreeMap<String, BTreeMap<String, String>>,
58    #[serde(default)]
59    pub links: Option<String>,
60}
61
62#[derive(Serialize)]
63pub struct NewCrateDependency {
64    pub optional: bool,
65    pub default_features: bool,
66    pub name: String,
67    pub features: Vec<String>,
68    pub version_req: String,
69    pub target: Option<String>,
70    pub kind: String,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub registry: Option<String>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub explicit_name_in_toml: Option<String>,
75}
76
77#[derive(Deserialize)]
78pub struct User {
79    pub id: u32,
80    pub login: String,
81    pub avatar: Option<String>,
82    pub email: Option<String>,
83    pub name: Option<String>,
84}
85
86pub struct Warnings {
87    pub invalid_categories: Vec<String>,
88    pub invalid_badges: Vec<String>,
89    pub other: Vec<String>,
90}
91
92#[derive(Deserialize)]
93struct R {
94    ok: bool,
95}
96#[derive(Deserialize)]
97struct OwnerResponse {
98    ok: bool,
99    msg: String,
100}
101#[derive(Deserialize)]
102struct ApiErrorList {
103    errors: Vec<ApiError>,
104}
105#[derive(Deserialize)]
106struct ApiError {
107    detail: String,
108}
109#[derive(Serialize)]
110struct OwnersReq<'a> {
111    users: &'a [&'a str],
112}
113#[derive(Deserialize)]
114struct Users {
115    users: Vec<User>,
116}
117#[derive(Deserialize)]
118struct TotalCrates {
119    total: u32,
120}
121#[derive(Deserialize)]
122struct Crates {
123    crates: Vec<Crate>,
124    meta: TotalCrates,
125}
126impl Registry {
127    pub fn new(host: String, token: Option<String>) -> Registry {
128        Registry::new_handle(host, token, Easy::new())
129    }
130
131    pub fn new_handle(host: String, token: Option<String>, handle: Easy) -> Registry {
132        Registry {
133            host,
134            token,
135            handle,
136        }
137    }
138
139    pub fn host(&self) -> &str {
140        &self.host
141    }
142
143    pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> Result<String> {
144        let body = serde_json::to_string(&OwnersReq { users: owners })?;
145        let body = self.put(&format!("/crates/{}/owners", krate), body.as_bytes())?;
146        assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
147        Ok(serde_json::from_str::<OwnerResponse>(&body)?.msg)
148    }
149
150    pub fn remove_owners(&mut self, krate: &str, owners: &[&str]) -> Result<()> {
151        let body = serde_json::to_string(&OwnersReq { users: owners })?;
152        let body = self.delete(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
153        assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
154        Ok(())
155    }
156
157    pub fn list_owners(&mut self, krate: &str) -> Result<Vec<User>> {
158        let body = self.get(&format!("/crates/{}/owners", krate))?;
159        Ok(serde_json::from_str::<Users>(&body)?.users)
160    }
161
162    pub fn publish(&mut self, krate: &NewCrate, tarball: &File) -> Result<Warnings> {
163        let json = serde_json::to_string(krate)?;
164        // Prepare the body. The format of the upload request is:
165        //
166        //      <le u32 of json>
167        //      <json request> (metadata for the package)
168        //      <le u32 of tarball>
169        //      <source tarball>
170        let stat = tarball.metadata()?;
171        let header = {
172            let mut w = Vec::new();
173            w.extend(
174                [
175                    (json.len() >> 0) as u8,
176                    (json.len() >> 8) as u8,
177                    (json.len() >> 16) as u8,
178                    (json.len() >> 24) as u8,
179                ].iter().cloned(),
180            );
181            w.extend(json.as_bytes().iter().cloned());
182            w.extend(
183                [
184                    (stat.len() >> 0) as u8,
185                    (stat.len() >> 8) as u8,
186                    (stat.len() >> 16) as u8,
187                    (stat.len() >> 24) as u8,
188                ].iter().cloned(),
189            );
190            w
191        };
192        let size = stat.len() as usize + header.len();
193        let mut body = Cursor::new(header).chain(tarball);
194
195        let url = format!("{}/api/v1/crates/new", self.host);
196
197        let token = match self.token.as_ref() {
198            Some(s) => s,
199            None => bail!("no upload token found, please run `cargo login`"),
200        };
201        self.handle.put(true)?;
202        self.handle.url(&url)?;
203        self.handle.in_filesize(size as u64)?;
204        let mut headers = List::new();
205        headers.append("Accept: application/json")?;
206        headers.append(&format!("Authorization: {}", token))?;
207        self.handle.http_headers(headers)?;
208
209        let body = handle(&mut self.handle, &mut |buf| body.read(buf).unwrap_or(0))?;
210
211        let response = if body.is_empty() {
212            "{}".parse()?
213        } else {
214            body.parse::<serde_json::Value>()?
215        };
216
217        let invalid_categories: Vec<String> = response
218            .get("warnings")
219            .and_then(|j| j.get("invalid_categories"))
220            .and_then(|j| j.as_array())
221            .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
222            .unwrap_or_else(Vec::new);
223
224        let invalid_badges: Vec<String> = response
225            .get("warnings")
226            .and_then(|j| j.get("invalid_badges"))
227            .and_then(|j| j.as_array())
228            .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
229            .unwrap_or_else(Vec::new);
230
231        let other: Vec<String> = response
232            .get("warnings")
233            .and_then(|j| j.get("other"))
234            .and_then(|j| j.as_array())
235            .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
236            .unwrap_or_else(Vec::new);
237
238        Ok(Warnings {
239            invalid_categories,
240            invalid_badges,
241            other,
242        })
243    }
244
245    pub fn search(&mut self, query: &str, limit: u32) -> Result<(Vec<Crate>, u32)> {
246        let formatted_query = percent_encode(query.as_bytes(), QUERY_ENCODE_SET);
247        let body = self.req(
248            &format!("/crates?q={}&per_page={}", formatted_query, limit),
249            None,
250            Auth::Unauthorized,
251        )?;
252
253        let crates = serde_json::from_str::<Crates>(&body)?;
254        Ok((crates.crates, crates.meta.total))
255    }
256
257    pub fn yank(&mut self, krate: &str, version: &str) -> Result<()> {
258        let body = self.delete(&format!("/crates/{}/{}/yank", krate, version), None)?;
259        assert!(serde_json::from_str::<R>(&body)?.ok);
260        Ok(())
261    }
262
263    pub fn unyank(&mut self, krate: &str, version: &str) -> Result<()> {
264        let body = self.put(&format!("/crates/{}/{}/unyank", krate, version), &[])?;
265        assert!(serde_json::from_str::<R>(&body)?.ok);
266        Ok(())
267    }
268
269    fn put(&mut self, path: &str, b: &[u8]) -> Result<String> {
270        self.handle.put(true)?;
271        self.req(path, Some(b), Auth::Authorized)
272    }
273
274    fn get(&mut self, path: &str) -> Result<String> {
275        self.handle.get(true)?;
276        self.req(path, None, Auth::Authorized)
277    }
278
279    fn delete(&mut self, path: &str, b: Option<&[u8]>) -> Result<String> {
280        self.handle.custom_request("DELETE")?;
281        self.req(path, b, Auth::Authorized)
282    }
283
284    fn req(&mut self, path: &str, body: Option<&[u8]>, authorized: Auth) -> Result<String> {
285        self.handle.url(&format!("{}/api/v1{}", self.host, path))?;
286        let mut headers = List::new();
287        headers.append("Accept: application/json")?;
288        headers.append("Content-Type: application/json")?;
289
290        if authorized == Auth::Authorized {
291            let token = match self.token.as_ref() {
292                Some(s) => s,
293                None => bail!("no upload token found, please run `cargo login`"),
294            };
295            headers.append(&format!("Authorization: {}", token))?;
296        }
297        self.handle.http_headers(headers)?;
298        match body {
299            Some(mut body) => {
300                self.handle.upload(true)?;
301                self.handle.in_filesize(body.len() as u64)?;
302                handle(&mut self.handle, &mut |buf| body.read(buf).unwrap_or(0))
303            }
304            None => handle(&mut self.handle, &mut |_| 0),
305        }
306    }
307}
308
309fn handle(handle: &mut Easy, read: &mut dyn FnMut(&mut [u8]) -> usize) -> Result<String> {
310    let mut headers = Vec::new();
311    let mut body = Vec::new();
312    {
313        let mut handle = handle.transfer();
314        handle.read_function(|buf| Ok(read(buf)))?;
315        handle.write_function(|data| {
316            body.extend_from_slice(data);
317            Ok(data.len())
318        })?;
319        handle.header_function(|data| {
320            headers.push(String::from_utf8_lossy(data).into_owned());
321            true
322        })?;
323        handle.perform()?;
324    }
325
326    match handle.response_code()? {
327        0 => {} // file upload url sometimes
328        200 => {}
329        403 => bail!("received 403 unauthorized response code"),
330        404 => bail!("received 404 not found response code"),
331        code => bail!(
332            "failed to get a 200 OK response, got {}\n\
333             headers:\n\
334             \t{}\n\
335             body:\n\
336             {}",
337            code,
338            headers.join("\n\t"),
339            String::from_utf8_lossy(&body)
340        ),
341    }
342
343    let body = match String::from_utf8(body) {
344        Ok(body) => body,
345        Err(..) => bail!("response body was not valid utf-8"),
346    };
347    if let Ok(errors) = serde_json::from_str::<ApiErrorList>(&body) {
348        let errors = errors.errors.into_iter().map(|s| s.detail);
349        bail!("api errors: {}", errors.collect::<Vec<_>>().join(", "));
350    }
351    Ok(body)
352}