1
1
#![ allow( clippy:: all) ]
2
2
3
3
use std:: collections:: BTreeMap ;
4
- use std:: fmt;
5
4
use std:: fs:: File ;
6
5
use std:: io:: prelude:: * ;
7
6
use std:: io:: { Cursor , SeekFrom } ;
8
7
use std:: time:: Instant ;
9
8
10
- use anyhow:: { bail, format_err, Context , Result } ;
11
9
use curl:: easy:: { Easy , List } ;
12
10
use percent_encoding:: { percent_encode, NON_ALPHANUMERIC } ;
13
11
use serde:: { Deserialize , Serialize } ;
14
12
use url:: Url ;
15
13
14
+ pub type Result < T > = std:: result:: Result < T , Error > ;
15
+
16
16
pub struct Registry {
17
17
/// The base URL for issuing API requests.
18
18
host : String ,
@@ -125,67 +125,62 @@ struct Crates {
125
125
meta : TotalCrates ,
126
126
}
127
127
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
+ ) ]
131
154
Api {
132
155
code : u32 ,
156
+ headers : Vec < String > ,
133
157
errors : Vec < String > ,
134
158
} ,
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
+ ) ]
135
165
Code {
136
166
code : u32 ,
137
167
headers : Vec < String > ,
138
168
body : String ,
139
169
} ,
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
- }
153
170
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 ) ,
189
184
}
190
185
191
186
impl Registry {
@@ -221,10 +216,9 @@ impl Registry {
221
216
}
222
217
223
218
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
+ } ) ?;
228
222
check_token ( token) ?;
229
223
Ok ( token)
230
224
}
@@ -270,12 +264,8 @@ impl Registry {
270
264
// This checks the length using seeking instead of metadata, because
271
265
// on some filesystems, getting the metadata will fail because
272
266
// 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 ) ) ?;
279
269
let header = {
280
270
let mut w = Vec :: new ( ) ;
281
271
w. extend ( & ( json. len ( ) as u32 ) . to_le_bytes ( ) ) ;
@@ -300,18 +290,12 @@ impl Registry {
300
290
let body = self
301
291
. handle ( & mut |buf| body. read ( buf) . unwrap_or ( 0 ) )
302
292
. map_err ( |e| match e {
303
- ResponseError :: Code { code, .. }
293
+ Error :: Code { code, .. }
304
294
if code == 503
305
295
&& started. elapsed ( ) . as_secs ( ) >= 29
306
296
&& self . host_is_crates_io ( ) =>
307
297
{
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)
315
299
}
316
300
_ => e. into ( ) ,
317
301
} ) ?;
@@ -410,10 +394,7 @@ impl Registry {
410
394
}
411
395
}
412
396
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 > {
417
398
let mut headers = Vec :: new ( ) ;
418
399
let mut body = Vec :: new ( ) ;
419
400
{
@@ -427,28 +408,29 @@ impl Registry {
427
408
// Headers contain trailing \r\n, trim them to make it easier
428
409
// to work with.
429
410
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
+ }
430
415
headers. push ( s) ;
431
416
true
432
417
} ) ?;
433
418
handle. perform ( ) ?;
434
419
}
435
420
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) ?;
444
422
let errors = serde_json:: from_str :: < ApiErrorList > ( & body)
445
423
. ok ( )
446
424
. map ( |s| s. errors . into_iter ( ) . map ( |s| s. detail ) . collect :: < Vec < _ > > ( ) ) ;
447
425
448
426
match ( self . handle . response_code ( ) ?, errors) {
449
427
( 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 {
452
434
code,
453
435
headers,
454
436
body,
@@ -457,6 +439,15 @@ impl Registry {
457
439
}
458
440
}
459
441
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
+
460
451
fn reason ( code : u32 ) -> & ' static str {
461
452
// Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
462
453
match code {
@@ -520,7 +511,7 @@ pub fn is_url_crates_io(url: &str) -> bool {
520
511
/// registries only create tokens in that format so that is as less restricted as possible.
521
512
pub fn check_token ( token : & str ) -> Result < ( ) > {
522
513
if token. is_empty ( ) {
523
- bail ! ( "please provide a non-empty token" ) ;
514
+ return Err ( Error :: InvalidToken ( "please provide a non-empty token" ) ) ;
524
515
}
525
516
if token. bytes ( ) . all ( |b| {
526
517
// This is essentially the US-ASCII limitation of
@@ -531,9 +522,9 @@ pub fn check_token(token: &str) -> Result<()> {
531
522
} ) {
532
523
Ok ( ( ) )
533
524
} else {
534
- Err ( anyhow :: anyhow! (
525
+ Err ( Error :: InvalidToken (
535
526
"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.",
537
528
) )
538
529
}
539
530
}
0 commit comments