@@ -50,7 +50,7 @@ pub use secret::Secret;
50
50
use stdio:: stdin_stdout_to_console;
51
51
52
52
/// Message sent by the credential helper on startup
53
- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
53
+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
54
54
pub struct CredentialHello {
55
55
// Protocol versions supported by the credential process.
56
56
pub v : Vec < u32 > ,
@@ -70,7 +70,7 @@ impl Credential for UnsupportedCredential {
70
70
}
71
71
72
72
/// Message sent by Cargo to the credential helper after the hello
73
- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
73
+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
74
74
#[ serde( rename_all = "kebab-case" ) ]
75
75
pub struct CredentialRequest < ' a > {
76
76
// Cargo will respond with the highest common protocol supported by both.
@@ -80,23 +80,25 @@ pub struct CredentialRequest<'a> {
80
80
#[ serde( borrow, flatten) ]
81
81
pub action : Action < ' a > ,
82
82
/// Additional command-line arguments passed to the credential provider.
83
+ #[ serde( skip_serializing_if = "Vec::is_empty" , default ) ]
83
84
pub args : Vec < & ' a str > ,
84
85
}
85
86
86
- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
87
+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
87
88
#[ serde( rename_all = "kebab-case" ) ]
88
89
pub struct RegistryInfo < ' a > {
89
90
/// Registry index url
90
91
pub index_url : & ' a str ,
91
92
/// Name of the registry in configuration. May not be available.
92
93
/// The crates.io registry will be `crates-io` (`CRATES_IO_REGISTRY`).
94
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
93
95
pub name : Option < & ' a str > ,
94
96
/// Headers from attempting to access a registry that resulted in a HTTP 401.
95
97
#[ serde( skip_serializing_if = "Vec::is_empty" , default ) ]
96
98
pub headers : Vec < String > ,
97
99
}
98
100
99
- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
101
+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
100
102
#[ non_exhaustive]
101
103
#[ serde( tag = "kind" , rename_all = "kebab-case" ) ]
102
104
pub enum Action < ' a > {
@@ -119,17 +121,19 @@ impl<'a> Display for Action<'a> {
119
121
}
120
122
}
121
123
122
- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
124
+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
123
125
#[ serde( rename_all = "kebab-case" ) ]
124
126
pub struct LoginOptions < ' a > {
125
127
/// Token passed on the command line via --token or from stdin
128
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
126
129
pub token : Option < Secret < & ' a str > > ,
127
130
/// Optional URL that the user can visit to log in to the registry
131
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
128
132
pub login_url : Option < & ' a str > ,
129
133
}
130
134
131
135
/// A record of what kind of operation is happening that we should generate a token for.
132
- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
136
+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
133
137
#[ non_exhaustive]
134
138
#[ serde( tag = "operation" , rename_all = "kebab-case" ) ]
135
139
pub enum Operation < ' a > {
@@ -168,12 +172,13 @@ pub enum Operation<'a> {
168
172
}
169
173
170
174
/// Message sent by the credential helper
171
- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
175
+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
172
176
#[ serde( tag = "kind" , rename_all = "kebab-case" ) ]
173
177
#[ non_exhaustive]
174
178
pub enum CredentialResponse {
175
179
Get {
176
180
token : Secret < String > ,
181
+ #[ serde( flatten) ]
177
182
cache : CacheControl ,
178
183
operation_independent : bool ,
179
184
} ,
@@ -183,14 +188,17 @@ pub enum CredentialResponse {
183
188
Unknown ,
184
189
}
185
190
186
- #[ derive( Serialize , Deserialize , Clone , Debug ) ]
187
- #[ serde( rename_all = "kebab-case" ) ]
191
+ #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , Eq ) ]
192
+ #[ serde( tag = "cache" , rename_all = "kebab-case" ) ]
188
193
#[ non_exhaustive]
189
194
pub enum CacheControl {
190
195
/// Do not cache this result.
191
196
Never ,
192
197
/// Cache this result and use it for subsequent requests in the current Cargo invocation until the specified time.
193
- Expires ( #[ serde( with = "time::serde::timestamp" ) ] OffsetDateTime ) ,
198
+ Expires {
199
+ #[ serde( with = "time::serde::timestamp" ) ]
200
+ expiration : OffsetDateTime ,
201
+ } ,
194
202
/// Cache this result and use it for all subsequent requests in the current Cargo invocation.
195
203
Session ,
196
204
#[ serde( other) ]
@@ -238,11 +246,7 @@ fn doit(
238
246
if len == 0 {
239
247
return Ok ( ( ) ) ;
240
248
}
241
- let request: CredentialRequest = serde_json:: from_str ( & buffer) ?;
242
- if request. v != PROTOCOL_VERSION_1 {
243
- return Err ( format ! ( "unsupported protocol version {}" , request. v) . into ( ) ) ;
244
- }
245
-
249
+ let request = deserialize_request ( & buffer) ?;
246
250
let response = stdin_stdout_to_console ( || {
247
251
credential. perform ( & request. registry , & request. action , & request. args )
248
252
} ) ?;
@@ -252,6 +256,17 @@ fn doit(
252
256
}
253
257
}
254
258
259
+ /// Deserialize a request from Cargo.
260
+ fn deserialize_request (
261
+ value : & str ,
262
+ ) -> Result < CredentialRequest < ' _ > , Box < dyn std:: error:: Error + Send + Sync > > {
263
+ let request: CredentialRequest = serde_json:: from_str ( & value) ?;
264
+ if request. v != PROTOCOL_VERSION_1 {
265
+ return Err ( format ! ( "unsupported protocol version {}" , request. v) . into ( ) ) ;
266
+ }
267
+ Ok ( request)
268
+ }
269
+
255
270
/// Read a line of text from stdin.
256
271
pub fn read_line ( ) -> Result < String , io:: Error > {
257
272
let mut buf = String :: new ( ) ;
@@ -278,3 +293,142 @@ pub fn read_token(
278
293
279
294
Ok ( Secret :: from ( read_line ( ) . map_err ( Box :: new) ?) )
280
295
}
296
+
297
+ #[ cfg( test) ]
298
+ mod tests {
299
+ use super :: * ;
300
+
301
+ #[ test]
302
+ fn unsupported_version ( ) {
303
+ // This shouldn't ever happen in practice, since the credential provider signals to Cargo which
304
+ // protocol versions it supports, and Cargo should only attempt to use one of those.
305
+ let msg = r#"{"v":999, "registry": {"index-url":""}, "args":[], "kind": "unexpected"}"# ;
306
+ assert_eq ! (
307
+ "unsupported protocol version 999" ,
308
+ deserialize_request( msg) . unwrap_err( ) . to_string( )
309
+ ) ;
310
+ }
311
+
312
+ #[ test]
313
+ fn cache_control ( ) {
314
+ let cc = CacheControl :: Expires {
315
+ expiration : OffsetDateTime :: from_unix_timestamp ( 1693928537 ) . unwrap ( ) ,
316
+ } ;
317
+ let json = serde_json:: to_string ( & cc) . unwrap ( ) ;
318
+ assert_eq ! ( json, r#"{"cache":"expires","expiration":1693928537}"# ) ;
319
+
320
+ let cc = CacheControl :: Session ;
321
+ let json = serde_json:: to_string ( & cc) . unwrap ( ) ;
322
+ assert_eq ! ( json, r#"{"cache":"session"}"# ) ;
323
+
324
+ let cc: CacheControl = serde_json:: from_str ( r#"{"cache":"unknown-kind"}"# ) . unwrap ( ) ;
325
+ assert_eq ! ( cc, CacheControl :: Unknown ) ;
326
+
327
+ assert_eq ! (
328
+ "missing field `expiration`" ,
329
+ serde_json:: from_str:: <CacheControl >( r#"{"cache":"expires"}"# )
330
+ . unwrap_err( )
331
+ . to_string( )
332
+ ) ;
333
+ }
334
+
335
+ #[ test]
336
+ fn credential_response ( ) {
337
+ let cr = CredentialResponse :: Get {
338
+ cache : CacheControl :: Never ,
339
+ operation_independent : true ,
340
+ token : Secret :: from ( "value" . to_string ( ) ) ,
341
+ } ;
342
+ let json = serde_json:: to_string ( & cr) . unwrap ( ) ;
343
+ assert_eq ! (
344
+ json,
345
+ r#"{"kind":"get","token":"value","cache":"never","operation_independent":true}"#
346
+ ) ;
347
+
348
+ let cr = CredentialResponse :: Login ;
349
+ let json = serde_json:: to_string ( & cr) . unwrap ( ) ;
350
+ assert_eq ! ( json, r#"{"kind":"login"}"# ) ;
351
+
352
+ let cr: CredentialResponse =
353
+ serde_json:: from_str ( r#"{"kind":"unknown-kind","extra-data":true}"# ) . unwrap ( ) ;
354
+ assert_eq ! ( cr, CredentialResponse :: Unknown ) ;
355
+
356
+ let cr: CredentialResponse =
357
+ serde_json:: from_str ( r#"{"kind":"login","extra-data":true}"# ) . unwrap ( ) ;
358
+ assert_eq ! ( cr, CredentialResponse :: Login ) ;
359
+
360
+ let cr: CredentialResponse = serde_json:: from_str ( r#"{"kind":"get","token":"value","cache":"never","operation_independent":true,"extra-field-ignored":123}"# ) . unwrap ( ) ;
361
+ assert_eq ! (
362
+ cr,
363
+ CredentialResponse :: Get {
364
+ cache: CacheControl :: Never ,
365
+ operation_independent: true ,
366
+ token: Secret :: from( "value" . to_string( ) )
367
+ }
368
+ ) ;
369
+ }
370
+
371
+ #[ test]
372
+ fn credential_request ( ) {
373
+ let get_oweners = CredentialRequest {
374
+ v : PROTOCOL_VERSION_1 ,
375
+ args : vec ! [ ] ,
376
+ registry : RegistryInfo {
377
+ index_url : "url" ,
378
+ name : None ,
379
+ headers : vec ! [ ] ,
380
+ } ,
381
+ action : Action :: Get ( Operation :: Owners { name : "pkg" } ) ,
382
+ } ;
383
+
384
+ let json = serde_json:: to_string ( & get_oweners) . unwrap ( ) ;
385
+ assert_eq ! (
386
+ json,
387
+ r#"{"v":1,"registry":{"index-url":"url"},"kind":"get","operation":"owners","name":"pkg"}"#
388
+ ) ;
389
+
390
+ let cr: CredentialRequest =
391
+ serde_json:: from_str ( r#"{"extra-1":true,"v":1,"registry":{"index-url":"url","extra-2":true},"kind":"get","operation":"owners","name":"pkg","args":[]}"# ) . unwrap ( ) ;
392
+ assert_eq ! ( cr, get_oweners) ;
393
+ }
394
+
395
+ #[ test]
396
+ fn credential_request_logout ( ) {
397
+ let unknown = CredentialRequest {
398
+ v : PROTOCOL_VERSION_1 ,
399
+ args : vec ! [ ] ,
400
+ registry : RegistryInfo {
401
+ index_url : "url" ,
402
+ name : None ,
403
+ headers : vec ! [ ] ,
404
+ } ,
405
+ action : Action :: Logout ,
406
+ } ;
407
+
408
+ let cr: CredentialRequest = serde_json:: from_str (
409
+ r#"{"v":1,"registry":{"index-url":"url"},"kind":"logout","extra-1":true,"args":[]}"# ,
410
+ )
411
+ . unwrap ( ) ;
412
+ assert_eq ! ( cr, unknown) ;
413
+ }
414
+
415
+ #[ test]
416
+ fn credential_request_unknown ( ) {
417
+ let unknown = CredentialRequest {
418
+ v : PROTOCOL_VERSION_1 ,
419
+ args : vec ! [ ] ,
420
+ registry : RegistryInfo {
421
+ index_url : "" ,
422
+ name : None ,
423
+ headers : vec ! [ ] ,
424
+ } ,
425
+ action : Action :: Unknown ,
426
+ } ;
427
+
428
+ let cr: CredentialRequest = serde_json:: from_str (
429
+ r#"{"v":1,"registry":{"index-url":""},"kind":"unexpected-1","extra-1":true,"args":[]}"# ,
430
+ )
431
+ . unwrap ( ) ;
432
+ assert_eq ! ( cr, unknown) ;
433
+ }
434
+ }
0 commit comments