11#![ allow( clippy:: all) ]
22
33use std:: collections:: BTreeMap ;
4- use std:: fmt;
54use std:: fs:: File ;
65use std:: io:: prelude:: * ;
76use std:: io:: { Cursor , SeekFrom } ;
87use std:: time:: Instant ;
98
10- use anyhow:: { bail, format_err, Context , Result } ;
119use curl:: easy:: { Easy , List } ;
1210use percent_encoding:: { percent_encode, NON_ALPHANUMERIC } ;
1311use serde:: { Deserialize , Serialize } ;
1412use url:: Url ;
1513
14+ pub type Result < T > = std:: result:: Result < T , Error > ;
15+
1616pub struct Registry {
1717 /// The base URL for issuing API requests.
1818 host : String ,
@@ -125,67 +125,62 @@ struct Crates {
125125 meta : TotalCrates ,
126126}
127127
128- #[ derive( Debug ) ]
129- pub enum ResponseError {
130- Curl ( curl:: Error ) ,
128+ /// Error returned when interacting with a registry.
129+ #[ derive( Debug , thiserror:: Error ) ]
130+ pub enum Error {
131+ /// Error from libcurl.
132+ #[ error( transparent) ]
133+ Curl ( #[ from] curl:: Error ) ,
134+
135+ /// Error from seriailzing the request payload and deserialzing the
136+ /// response body (like response body didn't match expected structure).
137+ #[ error( transparent) ]
138+ Json ( #[ from] serde_json:: Error ) ,
139+
140+ /// Error from IO. Mostly from reading the tarball to upload.
141+ #[ error( "failed to seek tarball" ) ]
142+ Io ( #[ from] std:: io:: Error ) ,
143+
144+ /// Response body was not valid utf8.
145+ #[ error( "invalid response body from server" ) ]
146+ Utf8 ( #[ from] std:: string:: FromUtf8Error ) ,
147+
148+ /// Error from API response containing JSON field `errors.details`.
149+ #[ error(
150+ "the remote server responded with an error{}: {}" ,
151+ status( * code) ,
152+ errors. join( ", " ) ,
153+ ) ]
131154 Api {
132155 code : u32 ,
156+ headers : Vec < String > ,
133157 errors : Vec < String > ,
134158 } ,
159+
160+ /// Error from API response which didn't have pre-programmed `errors.details`.
161+ #[ error(
162+ "failed to get a 200 OK response, got {code}\n headers:\n \t {}\n body:\n {body}" ,
163+ headers. join( "\n \t " ) ,
164+ ) ]
135165 Code {
136166 code : u32 ,
137167 headers : Vec < String > ,
138168 body : String ,
139169 } ,
140- Other ( anyhow:: Error ) ,
141- }
142-
143- impl std:: error:: Error for ResponseError {
144- fn source ( & self ) -> Option < & ( dyn std:: error:: Error + ' static ) > {
145- match self {
146- ResponseError :: Curl ( ..) => None ,
147- ResponseError :: Api { .. } => None ,
148- ResponseError :: Code { .. } => None ,
149- ResponseError :: Other ( e) => Some ( e. as_ref ( ) ) ,
150- }
151- }
152- }
153170
154- impl fmt:: Display for ResponseError {
155- fn fmt ( & self , f : & mut fmt:: Formatter ) -> fmt:: Result {
156- match self {
157- ResponseError :: Curl ( e) => write ! ( f, "{}" , e) ,
158- ResponseError :: Api { code, errors } => {
159- f. write_str ( "the remote server responded with an error" ) ?;
160- if * code != 200 {
161- write ! ( f, " (status {} {})" , code, reason( * code) ) ?;
162- } ;
163- write ! ( f, ": {}" , errors. join( ", " ) )
164- }
165- ResponseError :: Code {
166- code,
167- headers,
168- body,
169- } => write ! (
170- f,
171- "failed to get a 200 OK response, got {}\n \
172- headers:\n \
173- \t {}\n \
174- body:\n \
175- {}",
176- code,
177- headers. join( "\n \t " ) ,
178- body
179- ) ,
180- ResponseError :: Other ( ..) => write ! ( f, "invalid response from server" ) ,
181- }
182- }
183- }
184-
185- impl From < curl:: Error > for ResponseError {
186- fn from ( error : curl:: Error ) -> Self {
187- ResponseError :: Curl ( error)
188- }
171+ /// Reason why the token was invalid.
172+ #[ error( "{0}" ) ]
173+ InvalidToken ( & ' static str ) ,
174+
175+ /// Server was unavailable and timeouted. Happened when uploading a way
176+ /// too large tarball to crates.io.
177+ #[ error(
178+ "Request timed out after 30 seconds. If you're trying to \
179+ upload a crate it may be too large. If the crate is under \
180+ 10MB in size, you can email [email protected] for assistance.\n \ 181+ Total size was {0}."
182+ ) ]
183+ Timeout ( u64 ) ,
189184}
190185
191186impl Registry {
@@ -221,10 +216,9 @@ impl Registry {
221216 }
222217
223218 fn token ( & self ) -> Result < & str > {
224- let token = match self . token . as_ref ( ) {
225- Some ( s) => s,
226- None => bail ! ( "no upload token found, please run `cargo login`" ) ,
227- } ;
219+ let token = self . token . as_ref ( ) . ok_or_else ( || {
220+ Error :: InvalidToken ( "no upload token found, please run `cargo login`" )
221+ } ) ?;
228222 check_token ( token) ?;
229223 Ok ( token)
230224 }
@@ -270,12 +264,8 @@ impl Registry {
270264 // This checks the length using seeking instead of metadata, because
271265 // on some filesystems, getting the metadata will fail because
272266 // the file was renamed in ops::package.
273- let tarball_len = tarball
274- . seek ( SeekFrom :: End ( 0 ) )
275- . with_context ( || "failed to seek tarball" ) ?;
276- tarball
277- . seek ( SeekFrom :: Start ( 0 ) )
278- . with_context ( || "failed to seek tarball" ) ?;
267+ let tarball_len = tarball. seek ( SeekFrom :: End ( 0 ) ) ?;
268+ tarball. seek ( SeekFrom :: Start ( 0 ) ) ?;
279269 let header = {
280270 let mut w = Vec :: new ( ) ;
281271 w. extend ( & ( json. len ( ) as u32 ) . to_le_bytes ( ) ) ;
@@ -300,18 +290,12 @@ impl Registry {
300290 let body = self
301291 . handle ( & mut |buf| body. read ( buf) . unwrap_or ( 0 ) )
302292 . map_err ( |e| match e {
303- ResponseError :: Code { code, .. }
293+ Error :: Code { code, .. }
304294 if code == 503
305295 && started. elapsed ( ) . as_secs ( ) >= 29
306296 && self . host_is_crates_io ( ) =>
307297 {
308- format_err ! (
309- "Request timed out after 30 seconds. If you're trying to \
310- upload a crate it may be too large. If the crate is under \
311- 10MB in size, you can email [email protected] for assistance.\n \ 312- Total size was {}.",
313- tarball_len
314- )
298+ Error :: Timeout ( tarball_len)
315299 }
316300 _ => e. into ( ) ,
317301 } ) ?;
@@ -410,10 +394,7 @@ impl Registry {
410394 }
411395 }
412396
413- fn handle (
414- & mut self ,
415- read : & mut dyn FnMut ( & mut [ u8 ] ) -> usize ,
416- ) -> std:: result:: Result < String , ResponseError > {
397+ fn handle ( & mut self , read : & mut dyn FnMut ( & mut [ u8 ] ) -> usize ) -> Result < String > {
417398 let mut headers = Vec :: new ( ) ;
418399 let mut body = Vec :: new ( ) ;
419400 {
@@ -427,28 +408,29 @@ impl Registry {
427408 // Headers contain trailing \r\n, trim them to make it easier
428409 // to work with.
429410 let s = String :: from_utf8_lossy ( data) . trim ( ) . to_string ( ) ;
411+ // Don't let server sneak extra lines anywhere.
412+ if s. contains ( '\n' ) {
413+ return true ;
414+ }
430415 headers. push ( s) ;
431416 true
432417 } ) ?;
433418 handle. perform ( ) ?;
434419 }
435420
436- let body = match String :: from_utf8 ( body) {
437- Ok ( body) => body,
438- Err ( ..) => {
439- return Err ( ResponseError :: Other ( format_err ! (
440- "response body was not valid utf-8"
441- ) ) )
442- }
443- } ;
421+ let body = String :: from_utf8 ( body) ?;
444422 let errors = serde_json:: from_str :: < ApiErrorList > ( & body)
445423 . ok ( )
446424 . map ( |s| s. errors . into_iter ( ) . map ( |s| s. detail ) . collect :: < Vec < _ > > ( ) ) ;
447425
448426 match ( self . handle . response_code ( ) ?, errors) {
449427 ( 0 , None ) | ( 200 , None ) => Ok ( body) ,
450- ( code, Some ( errors) ) => Err ( ResponseError :: Api { code, errors } ) ,
451- ( code, None ) => Err ( ResponseError :: Code {
428+ ( code, Some ( errors) ) => Err ( Error :: Api {
429+ code,
430+ headers,
431+ errors,
432+ } ) ,
433+ ( code, None ) => Err ( Error :: Code {
452434 code,
453435 headers,
454436 body,
@@ -457,6 +439,15 @@ impl Registry {
457439 }
458440}
459441
442+ fn status ( code : u32 ) -> String {
443+ if code == 200 {
444+ String :: new ( )
445+ } else {
446+ let reason = reason ( code) ;
447+ format ! ( " (status {code} {reason})" )
448+ }
449+ }
450+
460451fn reason ( code : u32 ) -> & ' static str {
461452 // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
462453 match code {
@@ -520,7 +511,7 @@ pub fn is_url_crates_io(url: &str) -> bool {
520511/// registries only create tokens in that format so that is as less restricted as possible.
521512pub fn check_token ( token : & str ) -> Result < ( ) > {
522513 if token. is_empty ( ) {
523- bail ! ( "please provide a non-empty token" ) ;
514+ return Err ( Error :: InvalidToken ( "please provide a non-empty token" ) ) ;
524515 }
525516 if token. bytes ( ) . all ( |b| {
526517 // This is essentially the US-ASCII limitation of
@@ -531,9 +522,9 @@ pub fn check_token(token: &str) -> Result<()> {
531522 } ) {
532523 Ok ( ( ) )
533524 } else {
534- Err ( anyhow :: anyhow! (
525+ Err ( Error :: InvalidToken (
535526 "token contains invalid characters.\n Only printable ISO-8859-1 characters \
536- are allowed as it is sent in a HTTPS header."
527+ are allowed as it is sent in a HTTPS header.",
537528 ) )
538529 }
539530}
0 commit comments