2
2
using NuGet . Packaging ;
3
3
using System . Collections . Immutable ;
4
4
using System . CommandLine ;
5
+ using System . Formats . Tar ;
5
6
using System . IO . Compression ;
6
7
using System . Reflection ;
7
8
using System . Reflection . Metadata ;
@@ -44,7 +45,7 @@ static int Main(string[] args)
44
45
var parallelismArgument = new CliOption < int > ( "-parallel" )
45
46
{
46
47
Description = "Amount of parallelism used while analyzing the builds." ,
47
- DefaultValueFactory = _ => 16 ,
48
+ DefaultValueFactory = _ => 8 ,
48
49
Required = true
49
50
} ;
50
51
var baselineArgument = new CliOption < string > ( "-baseline" )
@@ -345,6 +346,11 @@ private async Task EvaluatePackage(AssetMapping mapping)
345
346
static readonly ImmutableArray < string > IncludedAssemblyNameCheckFileExtensions = [ ".dll" , ".exe" ] ;
346
347
347
348
349
+ /// <summary>
350
+ /// Evaluate the contents of a mapping between two packages.
351
+ /// </summary>
352
+ /// <param name="mapping"></param>
353
+ /// <returns></returns>
348
354
public async Task EvaluatePackageContents ( AssetMapping mapping )
349
355
{
350
356
var diffNugetPackagePath = mapping . DiffFilePath ;
@@ -373,31 +379,41 @@ public async Task EvaluatePackageContents(AssetMapping mapping)
373
379
}
374
380
}
375
381
382
+ /// <summary>
383
+ /// Compare the file lists of packages, identifying missing and extra files.
384
+ /// </summary>
385
+ /// <param name="mapping"></param>
386
+ /// <param name="testPackageReader"></param>
387
+ /// <param name="baselinePackageReader"></param>
376
388
private async void ComparePackageFileLists ( AssetMapping mapping , PackageArchiveReader testPackageReader , PackageArchiveReader baselinePackageReader )
377
389
{
378
390
IEnumerable < string > baselineFiles = ( await baselinePackageReader . GetFilesAsync ( CancellationToken . None ) ) ;
379
391
IEnumerable < string > testFiles = ( await testPackageReader . GetFilesAsync ( CancellationToken . None ) ) ;
380
392
381
- var missingFiles = RemovePackageFilesToIgnore ( baselineFiles . Except ( testFiles ) ) ;
393
+ // Strip down the baseline and test files to remove version numbers.
394
+ var strippedBaselineFiles = baselineFiles . Select ( f => RemoveVersionsNormalized ( f ) ) . ToList ( ) ;
395
+ var strippedTestFiles = testFiles . Select ( f => RemoveVersionsNormalized ( f ) ) . ToList ( ) ;
396
+
397
+ var missingFiles = RemovePackageFilesToIgnore ( strippedBaselineFiles . Except ( strippedTestFiles ) ) ;
382
398
383
399
foreach ( var missingFile in missingFiles )
384
400
{
385
401
mapping . Issues . Add ( new Issue
386
402
{
387
403
IssueType = IssueType . MissingPackageContent ,
388
- Description = $ "Package ' { mapping . Id } ' is missing the following files in the VMR: { string . Join ( ", " , missingFile ) } "
404
+ Description = missingFile ,
389
405
} ) ;
390
406
}
391
407
392
408
// Compare the other way, and identify content in the VMR that is not in the baseline
393
- var extraFiles = RemovePackageFilesToIgnore ( testFiles . Except ( baselineFiles ) ) ;
409
+ var extraFiles = RemovePackageFilesToIgnore ( strippedTestFiles . Except ( strippedBaselineFiles ) ) ;
394
410
395
411
foreach ( var extraFile in extraFiles )
396
412
{
397
413
mapping . Issues . Add ( new Issue
398
414
{
399
415
IssueType = IssueType . ExtraPackageContent ,
400
- Description = $ "Package ' { mapping . Id } ' has extra files in the VMR: { string . Join ( ", " , extraFile ) } "
416
+ Description = extraFile
401
417
} ) ;
402
418
}
403
419
@@ -423,8 +439,8 @@ private static async Task ComparePackageAssemblyVersions(AssetMapping mapping, P
423
439
{
424
440
try
425
441
{
426
- using var baselineStream = await CopyStreamToSeekableStream ( baselinePackageReader . GetEntry ( fileName ) . Open ( ) ) ;
427
- using var testStream = await CopyStreamToSeekableStream ( testPackageReader . GetEntry ( fileName ) . Open ( ) ) ;
442
+ using var baselineStream = await CopyStreamToSeekableStreamAsync ( baselinePackageReader . GetEntry ( fileName ) . Open ( ) ) ;
443
+ using var testStream = await CopyStreamToSeekableStreamAsync ( testPackageReader . GetEntry ( fileName ) . Open ( ) ) ;
428
444
429
445
CompareAssemblyVersions ( mapping , fileName , baselineStream , testStream ) ;
430
446
}
@@ -435,7 +451,12 @@ private static async Task ComparePackageAssemblyVersions(AssetMapping mapping, P
435
451
}
436
452
}
437
453
438
- private static async Task < Stream > CopyStreamToSeekableStream ( Stream stream )
454
+ /// <summary>
455
+ /// Copies a stream from an archive to a seekable stream (MemoryStream).
456
+ /// </summary>
457
+ /// <param name="stream"></param>
458
+ /// <returns></returns>
459
+ private static async Task < Stream > CopyStreamToSeekableStreamAsync ( Stream stream )
439
460
{
440
461
var outputStream = new MemoryStream ( ) ;
441
462
await stream . CopyToAsync ( outputStream , CancellationToken . None ) ;
@@ -532,15 +553,164 @@ public async Task EvaluateBlobContents(AssetMapping mapping)
532
553
{
533
554
// Switch on the file type, and call a helper based on the type
534
555
535
- switch ( Path . GetExtension ( mapping . Id ) )
556
+ if ( mapping . Id . EndsWith ( ".zip" ) )
536
557
{
537
- case ".zip" :
538
- await CompareZipArchiveContents ( mapping ) ;
539
- break ;
540
- default :
541
- return ;
558
+ await CompareZipArchiveContents ( mapping ) ;
559
+ }
560
+ else if ( mapping . Id . EndsWith ( ".tar.gz" ) || mapping . Id . EndsWith ( ".tgz" ) )
561
+ {
562
+ await CompareTarArchiveContents ( mapping ) ;
542
563
}
543
564
}
565
+ private async Task CompareTarArchiveContents ( AssetMapping mapping )
566
+ {
567
+ var diffTarPath = mapping . DiffFilePath ;
568
+ var baselineTarPath = mapping . BaseBuildFilePath ;
569
+ // If either of the paths don't exist, we can't run this comparison
570
+ if ( diffTarPath == null || baselineTarPath == null )
571
+ {
572
+ return ;
573
+ }
574
+
575
+ try
576
+ {
577
+ // Get the file lists for the baseline and diff tar files
578
+ IEnumerable < string > baselineFiles = GetTarGzArchiveFileList ( baselineTarPath ) ;
579
+ IEnumerable < string > diffFiles = GetTarGzArchiveFileList ( diffTarPath ) ;
580
+
581
+ // Compare file lists
582
+ CompareBlobArchiveFileLists ( mapping , baselineFiles , diffFiles ) ;
583
+
584
+ // Compare assembly versions
585
+ await CompareTarGzAssemblyVersions ( mapping , baselineFiles , diffFiles ) ;
586
+ }
587
+ catch ( Exception e )
588
+ {
589
+ mapping . EvaluationErrors . Add ( e . ToString ( ) ) ;
590
+ }
591
+ }
592
+
593
+ private List < string > GetTarGzArchiveFileList ( string archivePath )
594
+ {
595
+ List < string > entries = new ( ) ;
596
+ using ( FileStream fileStream = File . OpenRead ( archivePath ) )
597
+ {
598
+ using ( GZipStream gzipStream = new GZipStream ( fileStream , CompressionMode . Decompress ) )
599
+ using ( TarReader reader = new TarReader ( gzipStream ) )
600
+ {
601
+ TarEntry entry ;
602
+ while ( ( entry = reader . GetNextEntry ( ) ) != null )
603
+ {
604
+ entries . Add ( entry . Name ) ;
605
+ }
606
+ }
607
+ }
608
+
609
+ return entries ;
610
+ }
611
+
612
+ /// <summary>
613
+ /// This method is called "USE ALL AVAILABLE MEMORY"
614
+ /// </summary>
615
+ /// <param name="mapping"></param>
616
+ /// <param name="baselineFiles"></param>
617
+ /// <param name="diffFiles"></param>
618
+ /// <returns></returns>
619
+ private async Task CompareTarGzAssemblyVersions ( AssetMapping mapping , IEnumerable < string > baselineFiles , IEnumerable < string > diffFiles )
620
+ {
621
+ // Get the list of common files and create a map of file->stream
622
+ var strippedBaselineFiles = baselineFiles . Select ( f => RemoveVersionsNormalized ( f ) ) . ToList ( ) ;
623
+ var strippedDiffFiles = diffFiles . Select ( f => RemoveVersionsNormalized ( f ) ) . ToList ( ) ;
624
+
625
+ var commonFiles = strippedBaselineFiles . Intersect ( strippedDiffFiles ) . ToHashSet ( ) ;
626
+
627
+ var baselineStreams = new Dictionary < string , Stream > ( ) ;
628
+ var diffStreams = new Dictionary < string , Stream > ( ) ;
629
+
630
+ using ( FileStream baseStream = File . OpenRead ( mapping . BaseBuildFilePath ) )
631
+ {
632
+ using ( FileStream diffStream = File . OpenRead ( mapping . DiffFilePath ) )
633
+ {
634
+ using ( GZipStream baseGzipStream = new GZipStream ( baseStream , CompressionMode . Decompress ) )
635
+ using ( TarReader baseReader = new TarReader ( baseGzipStream ) )
636
+ {
637
+ using ( GZipStream diffGzipStream = new GZipStream ( diffStream , CompressionMode . Decompress ) )
638
+ using ( TarReader diffReader = new TarReader ( diffGzipStream ) )
639
+ {
640
+ string nextBaseEntry = null ;
641
+ string nextDiffEntry = null ;
642
+ do
643
+ {
644
+ nextBaseEntry = await WalkNextCommon ( commonFiles , baseReader , baselineStreams ) ;
645
+ if ( nextBaseEntry != null )
646
+ {
647
+ CompareAvailableStreams ( mapping , baselineStreams , diffStreams , nextBaseEntry ) ;
648
+ }
649
+
650
+ nextDiffEntry = await WalkNextCommon ( commonFiles , diffReader , diffStreams ) ;
651
+ if ( nextDiffEntry != null )
652
+ {
653
+ CompareAvailableStreams ( mapping , baselineStreams , diffStreams , nextDiffEntry ) ;
654
+ }
655
+ }
656
+ while ( nextBaseEntry != null || nextDiffEntry != null ) ;
657
+
658
+ // If there are any remaining streams, create an evaluation error
659
+ if ( baselineStreams . Count > 0 || diffStreams . Count > 0 )
660
+ {
661
+ mapping . EvaluationErrors . Add ( "Failed to compare all tar entries." ) ;
662
+ }
663
+ }
664
+ }
665
+ }
666
+ }
667
+
668
+ // Walk the tar to the next entry that exists in both the base and the diff
669
+ static async Task < string > WalkNextCommon ( HashSet < string > commonFiles , TarReader reader , Dictionary < string , Stream > streams )
670
+ {
671
+ TarEntry baseEntry ;
672
+ while ( ( baseEntry = reader . GetNextEntry ( ) ) != null && baseEntry . DataStream != null )
673
+ {
674
+ string entryStripped = RemoveVersionsNormalized ( baseEntry . Name ) ;
675
+ // If the element lives in the common files hash set, then copy it to a memory stream.
676
+ // Do not close the stream.
677
+ if ( commonFiles . Contains ( entryStripped ) )
678
+ {
679
+ streams [ entryStripped ] = await CopyStreamToSeekableStreamAsync ( baseEntry . DataStream ) ;
680
+ return entryStripped ;
681
+ }
682
+ }
683
+ return null ;
684
+ }
685
+
686
+ // Given we have a new entry that is common between base and diff, attempt to do some comparisons.
687
+ void CompareAvailableStreams ( AssetMapping mapping , Dictionary < string , Stream > baselineStreams , Dictionary < string , Stream > diffStreams ,
688
+ string entry )
689
+ {
690
+ if ( baselineStreams . TryGetValue ( entry , out var baselineFileStream ) &&
691
+ diffStreams . TryGetValue ( entry , out var diffFileStream ) )
692
+ {
693
+ CompareAssemblyVersions ( mapping , entry , baselineFileStream , diffFileStream ) ;
694
+ baselineFileStream . Dispose ( ) ;
695
+ diffFileStream . Dispose ( ) ;
696
+ baselineStreams . Remove ( entry ) ;
697
+ diffStreams . Remove ( entry ) ;
698
+ }
699
+ }
700
+ }
701
+
702
+ private static string RemoveVersionsNormalized ( string path )
703
+ {
704
+ string strippedPath = path . Replace ( "\\ " , "//" ) ;
705
+ string prevPath = path ;
706
+ do
707
+ {
708
+ prevPath = strippedPath ;
709
+ strippedPath = VersionIdentifier . RemoveVersions ( strippedPath ) ;
710
+ } while ( prevPath != strippedPath ) ;
711
+
712
+ return strippedPath ;
713
+ }
544
714
545
715
private async Task CompareZipArchiveContents ( AssetMapping mapping )
546
716
{
@@ -586,8 +756,8 @@ private async Task CompareZipAssemblyVersions(AssetMapping mapping, ZipArchive d
586
756
{
587
757
try
588
758
{
589
- using var baselineStream = await CopyStreamToSeekableStream ( baselineArchive . GetEntry ( fileName ) . Open ( ) ) ;
590
- using var testStream = await CopyStreamToSeekableStream ( diffArchive . GetEntry ( fileName ) . Open ( ) ) ;
759
+ using var baselineStream = await CopyStreamToSeekableStreamAsync ( baselineArchive . GetEntry ( fileName ) . Open ( ) ) ;
760
+ using var testStream = await CopyStreamToSeekableStreamAsync ( diffArchive . GetEntry ( fileName ) . Open ( ) ) ;
591
761
592
762
CompareAssemblyVersions ( mapping , fileName , baselineStream , testStream ) ;
593
763
}
@@ -600,7 +770,16 @@ private async Task CompareZipAssemblyVersions(AssetMapping mapping, ZipArchive d
600
770
601
771
private static void CompareAssemblyVersions ( AssetMapping mapping , string fileName , Stream baselineStream , Stream testStream )
602
772
{
603
- AssemblyName baselineAssemblyName = GetAssemblyName ( baselineStream , fileName ) ;
773
+ AssemblyName baselineAssemblyName = null ;
774
+ try
775
+ {
776
+ baselineAssemblyName = GetAssemblyName ( baselineStream , fileName ) ;
777
+ }
778
+ catch ( BadImageFormatException )
779
+ {
780
+ // Assume the file is not an assembly, and then don't attempt for the test assembly
781
+ return ;
782
+ }
604
783
AssemblyName testAssemblyName = GetAssemblyName ( testStream , fileName ) ;
605
784
if ( ( baselineAssemblyName == null ) != ( testAssemblyName == null ) )
606
785
{
@@ -629,16 +808,16 @@ private static void CompareBlobArchiveFileLists(AssetMapping mapping, IEnumerabl
629
808
{
630
809
// Because these typically contain version numbers in their paths, we need to go and remove those.
631
810
632
- var strippedBaselineFiles = baselineFiles . Select ( f => VersionIdentifier . RemoveVersions ( f ) ) . ToList ( ) ;
633
- var strippedDiffFiles = diffFiles . Select ( f => VersionIdentifier . RemoveVersions ( f ) ) . ToList ( ) ;
811
+ var strippedBaselineFiles = baselineFiles . Select ( f => RemoveVersionsNormalized ( f ) ) . ToList ( ) ;
812
+ var strippedDiffFiles = diffFiles . Select ( f => RemoveVersionsNormalized ( f ) ) . ToList ( ) ;
634
813
635
814
var missingFiles = strippedBaselineFiles . Except ( strippedDiffFiles ) ;
636
815
foreach ( var missingFile in missingFiles )
637
816
{
638
817
mapping . Issues . Add ( new Issue
639
818
{
640
819
IssueType = IssueType . MissingPackageContent ,
641
- Description = $ "Blob ' { mapping . Id } ' is missing the following files in the VMR: { string . Join ( ", " , missingFile ) } "
820
+ Description = missingFile
642
821
} ) ;
643
822
}
644
823
// Compare the other way, and identify content in the VMR that is not in the baseline
@@ -648,7 +827,7 @@ private static void CompareBlobArchiveFileLists(AssetMapping mapping, IEnumerabl
648
827
mapping . Issues . Add ( new Issue
649
828
{
650
829
IssueType = IssueType . ExtraPackageContent ,
651
- Description = $ "Blob ' { mapping . Id } ' has extra files in the VMR: { string . Join ( ", " , extraFile ) } "
830
+ Description = extraFile
652
831
} ) ;
653
832
}
654
833
}
0 commit comments