3
3
#![ deny( missing_docs) ]
4
4
#![ warn( rust_2018_idioms) ]
5
5
6
- use std:: { borrow:: Cow , str:: FromStr } ;
6
+ use std:: { borrow:: Cow , num :: ParseIntError , str:: FromStr } ;
7
7
8
+ use documented:: Documented ;
8
9
use duplicate:: duplicate;
9
- use lazy_static:: lazy_static;
10
- use regex:: Regex ;
11
10
use serde:: { Deserialize , Serialize } ;
12
11
use strum:: { AsRefStr , Display , EnumIter , EnumString } ;
13
12
use utoipa:: ToSchema ;
@@ -23,13 +22,7 @@ pub use locator::*;
23
22
pub use locator_package:: * ;
24
23
pub use locator_strict:: * ;
25
24
26
- /// [`Locator`](crate::Locator) is closely tied with the concept of Core's "fetchers",
27
- /// which are asynchronous jobs tasked with downloading the code
28
- /// referred to by a [`Locator`](crate::Locator) so that Core or some other service
29
- /// may analyze it.
30
- ///
31
- /// For more information on the background of `Locator` and fetchers generally,
32
- /// refer to [Fetchers and Locators](https://go/fetchers-doc).
25
+ /// `Fetcher` identifies a supported code host protocol.
33
26
#[ derive(
34
27
Copy ,
35
28
Clone ,
@@ -45,10 +38,12 @@ pub use locator_strict::*;
45
38
AsRefStr ,
46
39
Serialize ,
47
40
Deserialize ,
41
+ Documented ,
48
42
ToSchema ,
49
43
) ]
50
44
#[ non_exhaustive]
51
45
#[ serde( rename_all = "snake_case" ) ]
46
+ #[ schema( example = json!( "git" ) ) ]
52
47
pub enum Fetcher {
53
48
/// Archive locators are FOSSA specific.
54
49
#[ strum( serialize = "archive" ) ]
@@ -176,7 +171,13 @@ pub enum Fetcher {
176
171
}
177
172
178
173
/// Identifies the organization to which this locator is namespaced.
179
- #[ derive( Copy , Clone , Eq , PartialEq , Ord , PartialOrd , Hash ) ]
174
+ ///
175
+ /// Organization IDs are canonically created by FOSSA instances
176
+ /// and have no meaning outside of FOSSA instances.
177
+ #[ derive(
178
+ Copy , Clone , Eq , PartialEq , Ord , PartialOrd , Serialize , Deserialize , Hash , Documented , ToSchema ,
179
+ ) ]
180
+ #[ schema( example = json!( 1 ) ) ]
180
181
pub struct OrgId ( usize ) ;
181
182
182
183
impl From < OrgId > for usize {
@@ -191,6 +192,15 @@ impl From<usize> for OrgId {
191
192
}
192
193
}
193
194
195
+ impl FromStr for OrgId {
196
+ type Err = ParseIntError ;
197
+
198
+ fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
199
+ let id = s. parse ( ) ?;
200
+ Ok ( Self ( id) )
201
+ }
202
+ }
203
+
194
204
duplicate ! {
195
205
[
196
206
number;
@@ -237,7 +247,16 @@ impl std::fmt::Debug for OrgId {
237
247
}
238
248
239
249
/// The package section of the locator.
240
- #[ derive( Clone , Eq , PartialEq , Hash ) ]
250
+ ///
251
+ /// A "package" is generally the name of a project or dependency in a code host.
252
+ /// However some fetcher protocols (such as `git`) embed additional information
253
+ /// inside the `Package` of a locator, such as the URL of the `git` instance
254
+ /// from which the project can be fetched.
255
+ ///
256
+ /// Additionally, some fetcher protocols (such as `apk`, `rpm-generic`, and `deb`)
257
+ /// further encode additional standardized information in the `Package` of the locator.
258
+ #[ derive( Clone , Eq , PartialEq , Hash , Serialize , Deserialize , Documented , ToSchema ) ]
259
+ #[ schema( example = json!( "github.com/fossas/locator-rs" ) ) ]
241
260
pub struct Package ( String ) ;
242
261
243
262
impl Package {
@@ -290,9 +309,15 @@ impl std::cmp::PartialOrd for Package {
290
309
}
291
310
292
311
/// The revision section of the locator.
293
- #[ derive( Clone , Eq , PartialEq , Hash ) ]
312
+ ///
313
+ /// A "revision" is the version of the project in the code host.
314
+ /// Some fetcher protocols (such as `apk`, `rpm-generic`, and `deb`)
315
+ /// encode additional standardized information in the `Revision` of the locator.
316
+ #[ derive( Clone , Eq , PartialEq , Hash , Documented , ToSchema ) ]
317
+ #[ schema( example = json!( "v1.0.0" ) ) ]
294
318
pub enum Revision {
295
319
/// The revision is valid semver.
320
+ #[ schema( value_type = String ) ]
296
321
Semver ( semver:: Version ) ,
297
322
298
323
/// The revision is an opaque string.
@@ -345,6 +370,24 @@ impl std::fmt::Debug for Revision {
345
370
}
346
371
}
347
372
373
+ impl Serialize for Revision {
374
+ fn serialize < S > ( & self , serializer : S ) -> Result < S :: Ok , S :: Error >
375
+ where
376
+ S : serde:: Serializer ,
377
+ {
378
+ self . to_string ( ) . serialize ( serializer)
379
+ }
380
+ }
381
+
382
+ impl < ' de > Deserialize < ' de > for Revision {
383
+ fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
384
+ where
385
+ D : serde:: Deserializer < ' de > ,
386
+ {
387
+ String :: deserialize ( deserializer) . map ( Self :: from)
388
+ }
389
+ }
390
+
348
391
impl std:: cmp:: Ord for Revision {
349
392
fn cmp ( & self , other : & Self ) -> std:: cmp:: Ordering {
350
393
let cmp = alphanumeric_sort:: compare_str;
@@ -364,46 +407,39 @@ impl std::cmp::PartialOrd for Revision {
364
407
}
365
408
366
409
/// Optionally parse an org ID and trimmed package out of a package string.
367
- fn parse_org_package ( package : & str ) -> Result < ( Option < OrgId > , Package ) , PackageParseError > {
368
- lazy_static ! {
369
- static ref RE : Regex = Regex :: new( r"^(?:(?P<org_id>\d+)/)?(?P<package>.+)" )
370
- . expect( "Package parsing expression must compile" ) ;
371
- }
372
-
373
- let mut captures = RE . captures_iter ( package) ;
374
- let capture = captures. next ( ) . ok_or_else ( || PackageParseError :: Package {
375
- package : package. to_string ( ) ,
376
- } ) ?;
377
-
378
- let trimmed_package =
379
- capture
380
- . name ( "package" )
381
- . map ( |m| m. as_str ( ) )
382
- . ok_or_else ( || PackageParseError :: Field {
383
- package : package. to_string ( ) ,
384
- field : String :: from ( "package" ) ,
385
- } ) ?;
386
-
387
- // If we fail to parse the org_id as a valid number, don't fail the overall parse;
388
- // just don't namespace to org ID and return the input unmodified.
389
- match capture
390
- . name ( "org_id" )
391
- . map ( |m| m. as_str ( ) )
392
- . map ( OrgId :: try_from)
393
- {
394
- // An org ID was provided and validly parsed, use it.
395
- Some ( Ok ( org_id) ) => Ok ( ( Some ( org_id) , Package :: from ( trimmed_package) ) ) ,
396
-
397
- // Otherwise, if we either didn't get an org ID section,
398
- // or it wasn't a valid org ID,
399
- // just use the package as-is.
400
- _ => Ok ( ( None , Package :: from ( package) ) ) ,
410
+ fn parse_org_package ( input : & str ) -> ( Option < OrgId > , Package ) {
411
+ macro_rules! construct {
412
+ ( $org_id: expr, $package: expr) => {
413
+ return ( Some ( $org_id) , Package :: from( $package) )
414
+ } ;
415
+ ( $package: expr) => {
416
+ return ( None , Package :: from( $package) )
417
+ } ;
401
418
}
419
+
420
+ // No `/`? This must not be namespaced.
421
+ let Some ( ( org_id, package) ) = input. split_once ( '/' ) else {
422
+ construct ! ( input) ;
423
+ } ;
424
+
425
+ // Nothing before or after the `/`? Still not namespaced.
426
+ if org_id. is_empty ( ) || package. is_empty ( ) {
427
+ construct ! ( input) ;
428
+ } ;
429
+
430
+ // If the part before the `/` isn't a number, it can't be a namespaced org id.
431
+ let Ok ( org_id) = org_id. parse ( ) else {
432
+ construct ! ( input)
433
+ } ;
434
+
435
+ // Ok, there was text before and after the `/`, and the content before was a number.
436
+ // Finally, we've got a namespaced package.
437
+ construct ! ( org_id, package)
402
438
}
403
439
404
440
#[ cfg( test) ]
405
441
mod tests {
406
- use itertools :: izip ;
442
+ use simple_test_case :: test_case ;
407
443
408
444
use super :: * ;
409
445
@@ -413,52 +449,91 @@ mod tests {
413
449
}
414
450
}
415
451
452
+ macro_rules! revision {
453
+ ( semver => $input: expr) => {
454
+ Revision :: Semver ( semver:: Version :: parse( $input) . expect( "parse semver" ) )
455
+ } ;
456
+ ( opaque => $input: expr) => {
457
+ Revision :: Opaque ( String :: from( $input) )
458
+ } ;
459
+ }
460
+
461
+ #[ test_case( "0/name" , Some ( OrgId ( 0 ) ) , Package :: new( "name" ) ; "0/name" ) ]
462
+ #[ test_case( "1/name" , Some ( OrgId ( 1 ) ) , Package :: new( "name" ) ; "1/name" ) ]
463
+ #[ test_case( "1/name/foo" , Some ( OrgId ( 1 ) ) , Package :: new( "name/foo" ) ; "1/name/foo" ) ]
464
+ #[ test_case( "1//name/foo" , Some ( OrgId ( 1 ) ) , Package :: new( "/name/foo" ) ; "doubleslash_1/name/foo" ) ]
465
+ #[ test_case( "9809572/name/foo" , Some ( OrgId ( 9809572 ) ) , Package :: new( "name/foo" ) ; "9809572/name/foo" ) ]
466
+ #[ test_case( "name/foo" , None , Package :: new( "name/foo" ) ; "name/foo" ) ]
467
+ #[ test_case( "name" , None , Package :: new( "name" ) ; "name" ) ]
468
+ #[ test_case( "/name/foo" , None , Package :: new( "/name/foo" ) ; "/name/foo" ) ]
469
+ #[ test_case( "/123/name/foo" , None , Package :: new( "/123/name/foo" ) ; "/123/name/foo" ) ]
470
+ #[ test_case( "/name" , None , Package :: new( "/name" ) ; "/name" ) ]
471
+ #[ test_case( "abcd/1234/name" , None , Package :: new( "abcd/1234/name" ) ; "abcd/1234/name" ) ]
472
+ #[ test_case( "1abc2/name" , None , Package :: new( "1abc2/name" ) ; "1abc2/name" ) ]
473
+ #[ test_case( "name/1234" , None , Package :: new( "name/1234" ) ; "name/1234" ) ]
416
474
#[ test]
417
- fn parses_org_package ( ) {
418
- let orgs = [ OrgId ( 0usize ) , OrgId ( 1 ) , OrgId ( 9809572 ) ] ;
419
- let names = [ Package :: new ( "name" ) , Package :: new ( "name/foo" ) ] ;
420
-
421
- for ( org, name) in izip ! ( orgs, names) {
422
- let test = format ! ( "{org}/{name}" ) ;
423
- let Ok ( ( Some ( org_id) , package) ) = parse_org_package ( & test) else {
424
- panic ! ( "must parse '{test}'" )
425
- } ;
426
- assert_eq ! ( org_id, org, "'org_id' must match in '{test}'" ) ;
427
- assert_eq ! ( package, name, "'package' must match in '{test}" ) ;
428
- }
475
+ fn parse_org_package ( input : & str , org : Option < OrgId > , package : Package ) {
476
+ let ( org_id, name) = parse_org_package ( input) ;
477
+ assert_eq ! ( org_id, org, "'org_id' must match in '{input}'" ) ;
478
+ assert_eq ! ( package, name, "'package' must match in '{input}" ) ;
429
479
}
430
480
481
+ #[ test_case( r#""rpm-generic""# , Fetcher :: LinuxRpm ; "rpm-generic" ) ]
482
+ #[ test_case( r#""deb""# , Fetcher :: LinuxDebian ; "deb" ) ]
483
+ #[ test_case( r#""apk""# , Fetcher :: LinuxAlpine ; "apk" ) ]
431
484
#[ test]
432
- fn parses_org_package_no_org ( ) {
433
- let names = [
434
- Package :: new ( "/name/foo" ) ,
435
- Package :: new ( "/name" ) ,
436
- Package :: new ( "abcd/1234/name" ) ,
437
- Package :: new ( "1abc2/name" ) ,
438
- ] ;
439
- for test in names {
440
- let input = & format ! ( "{test}" ) ;
441
- let Ok ( ( org_id, package) ) = parse_org_package ( input) else {
442
- panic ! ( "must parse '{test}'" )
443
- } ;
444
- assert_eq ! ( org_id, None , "'org_id' must be None in '{test}'" ) ;
445
- assert_eq ! ( package, test, "'package' must match in '{test}" ) ;
446
- }
485
+ fn serializes_linux_properly ( expected : & str , value : Fetcher ) {
486
+ assert_eq ! ( expected, serde_json:: to_string( & value) . unwrap( ) ) ;
487
+ }
488
+
489
+ #[ test_case( Package :: new( "name" ) ; "name" ) ]
490
+ #[ test_case( Package :: new( "name/foo" ) ; "name/foo" ) ]
491
+ #[ test_case( Package :: new( "/name/foo" ) ; "/name/foo" ) ]
492
+ #[ test_case( Package :: new( "/name" ) ; "/name" ) ]
493
+ #[ test_case( Package :: new( "abcd/1234/name" ) ; "abcd/1234/name" ) ]
494
+ #[ test_case( Package :: new( "1abc2/name" ) ; "1abc2/name" ) ]
495
+ #[ test]
496
+ fn package_roundtrip ( package : Package ) {
497
+ let serialized = serde_json:: to_string ( & package) . expect ( "must serialize" ) ;
498
+ let deserialized = serde_json:: from_str ( & serialized) . expect ( "must deserialize" ) ;
499
+ assert_eq ! ( package, deserialized) ;
500
+ }
501
+
502
+ #[ test_case( "1.0.0" , revision!( semver => "1.0.0" ) ; "1.0.0" ) ]
503
+ #[ test_case( "1.2.0" , revision!( semver => "1.2.0" ) ; "1.2.0" ) ]
504
+ #[ test_case( "1.0.0-alpha.1" , revision!( semver => "1.0.0-alpha.1" ) ; "1.0.0-alpha.1" ) ]
505
+ #[ test_case( "1.0.0-alpha1" , revision!( semver => "1.0.0-alpha1" ) ; "1.0.0-alpha1" ) ]
506
+ #[ test_case( "1.0.0-rc.10+r1234" , revision!( semver => "1.0.0-rc.10+r1234" ) ; "1.0.0-rc.10+r1234" ) ]
507
+ #[ test_case( "abcd1234" , revision!( opaque => "abcd1234" ) ; "abcd1234" ) ]
508
+ #[ test_case( "v1.0.0" , revision!( opaque => "v1.0.0" ) ; "v1.0.0" ) ]
509
+ #[ test]
510
+ fn revision ( revision : & str , expected : Revision ) {
511
+ let serialized = serde_json:: to_string ( & revision) . expect ( "must serialize" ) ;
512
+ let deserialized = serde_json:: from_str ( & serialized) . expect ( "must deserialize" ) ;
513
+ assert_eq ! ( expected, deserialized) ;
514
+ }
515
+
516
+ #[ test_case( Revision :: from( "1.0.0" ) ; "1.0.0" ) ]
517
+ #[ test_case( Revision :: from( "1.2.0" ) ; "1.2.0" ) ]
518
+ #[ test_case( Revision :: from( "1.0.0-alpha.1" ) ; "1.0.0-alpha.1" ) ]
519
+ #[ test_case( Revision :: from( "1.0.0-alpha1" ) ; "1.0.0-alpha1" ) ]
520
+ #[ test_case( Revision :: from( "1.0.0-rc.10" ) ; "1.0.0-rc.10" ) ]
521
+ #[ test_case( Revision :: from( "abcd1234" ) ; "abcd1234" ) ]
522
+ #[ test_case( Revision :: from( "v1.0.0" ) ; "v1.0.0" ) ]
523
+ #[ test]
524
+ fn revision_roundtrip ( revision : Revision ) {
525
+ let serialized = serde_json:: to_string ( & revision) . expect ( "must serialize" ) ;
526
+ let deserialized = serde_json:: from_str ( & serialized) . expect ( "must deserialize" ) ;
527
+ assert_eq ! ( revision, deserialized) ;
447
528
}
448
529
530
+ #[ test_case( OrgId ( 1 ) ; "1" ) ]
531
+ #[ test_case( OrgId ( 0 ) ; "0" ) ]
532
+ #[ test_case( OrgId ( 1210931039 ) ; "1210931039" ) ]
449
533
#[ test]
450
- fn serializes_linux_properly ( ) {
451
- assert_eq ! (
452
- r#""rpm-generic""# ,
453
- serde_json:: to_string( & Fetcher :: LinuxRpm ) . unwrap( )
454
- ) ;
455
- assert_eq ! (
456
- r#""deb""# ,
457
- serde_json:: to_string( & Fetcher :: LinuxDebian ) . unwrap( )
458
- ) ;
459
- assert_eq ! (
460
- r#""apk""# ,
461
- serde_json:: to_string( & Fetcher :: LinuxAlpine ) . unwrap( )
462
- ) ;
534
+ fn org_roundtrip ( org : OrgId ) {
535
+ let serialized = serde_json:: to_string ( & org) . expect ( "must serialize" ) ;
536
+ let deserialized = serde_json:: from_str ( & serialized) . expect ( "must deserialize" ) ;
537
+ assert_eq ! ( org, deserialized) ;
463
538
}
464
539
}
0 commit comments