66using System . IO ;
77using System . Linq ;
88using Microsoft . Sbom . Common ;
9+ using Microsoft . Sbom . Common . Utils ;
910using Microsoft . Sbom . Contracts ;
1011using Microsoft . Sbom . Extensions ;
1112using Microsoft . Sbom . Extensions . Entities ;
1213using Microsoft . Sbom . JsonAsynchronousNodeKit ;
1314using Microsoft . Sbom . Parser ;
1415using Microsoft . Sbom . Parsers . Spdx22SbomParser . Entities ;
16+ using Microsoft . Sbom . Utils ;
17+ using Serilog ;
1518
1619namespace Microsoft . Sbom . Parsers . Spdx22SbomParser ;
1720
@@ -20,11 +23,15 @@ namespace Microsoft.Sbom.Parsers.Spdx22SbomParser;
2023/// </summary>
2124public class MergeableContentProvider : IMergeableContentProvider
2225{
26+ private const string DependsOn = "DEPENDS_ON" ;
27+
2328 private readonly IFileSystemUtils fileSystemUtils ;
29+ private readonly ILogger logger ;
2430
25- public MergeableContentProvider ( IFileSystemUtils fileSystemUtils )
31+ public MergeableContentProvider ( IFileSystemUtils fileSystemUtils , ILogger logger )
2632 {
2733 this . fileSystemUtils = fileSystemUtils ?? throw new ArgumentNullException ( nameof ( fileSystemUtils ) ) ;
34+ this . logger = logger ?? throw new ArgumentNullException ( nameof ( logger ) ) ;
2835 }
2936
3037 /// <summary>
@@ -44,6 +51,7 @@ public bool TryGetContent(string filePath, out MergeableContent mergeableContent
4451
4552 if ( ! fileSystemUtils . FileExists ( filePath ) )
4653 {
54+ logger . Debug ( $ "File '{ filePath } ' does not exist.") ;
4755 mergeableContent = null ;
4856 return false ;
4957 }
@@ -52,16 +60,19 @@ public bool TryGetContent(string filePath, out MergeableContent mergeableContent
5260
5361 if ( stream == null )
5462 {
63+ logger . Debug ( $ "Unable to open stream for file '{ filePath } '.") ;
5564 mergeableContent = null ;
5665 return false ;
5766 }
5867
5968 try
6069 {
70+ logger . Debug ( $ "Attempting to parse SPDX file at '{ filePath } '.") ;
6171 return GetMergeableContent ( stream , out mergeableContent ) ;
6272 }
6373 catch ( Exception )
6474 {
75+ logger . Debug ( $ "Failed to parse SPDX file at '{ filePath } '. It may not be a valid SPDX 2.2 file.") ;
6576 mergeableContent = null ;
6677 return false ;
6778 }
@@ -73,9 +84,9 @@ public bool TryGetContent(string filePath, out MergeableContent mergeableContent
7384 private bool GetMergeableContent ( Stream stream , out MergeableContent mergeableContent )
7485 {
7586 // Skip sections that are expensive to parse and would require additional logic to properly ignore.
76- var parser = new SPDXParser ( stream , [ "files" , "externalDocumentRefs" , "relationships" ] ) ;
77- var packages = Enumerable . Empty < SbomPackage > ( ) ;
78- var relationships = Enumerable . Empty < SbomRelationship > ( ) ;
87+ var parser = new SPDXParser ( stream , new [ ] { "files" , "externalDocumentRefs" } ) ;
88+ IList < SbomPackage > packages = new List < SbomPackage > ( ) ;
89+ IList < SbomRelationship > relationships = new List < SbomRelationship > ( ) ;
7990
8091 ParserStateResult result = null ;
8192 do
@@ -86,7 +97,10 @@ private bool GetMergeableContent(Stream stream, out MergeableContent mergeableCo
8697 switch ( result )
8798 {
8899 case PackagesResult packagesResult :
89- packages = ProcessPackages ( packagesResult . Packages ) ;
100+ packages = ProcessPackages ( packagesResult . Packages . ToList ( ) ) ; // ToList() ensures correct parsing of the data
101+ break ;
102+ case RelationshipsResult relationshipsResult :
103+ relationships = ProcessRelationships ( relationshipsResult . Relationships . ToList ( ) ) ; // ToList() ensures correct parsing of the data
90104 break ;
91105 default :
92106 break ;
@@ -95,33 +109,105 @@ private bool GetMergeableContent(Stream stream, out MergeableContent mergeableCo
95109 }
96110 while ( result is not null ) ;
97111
98- mergeableContent = new MergeableContent ( packages , relationships ) ;
112+ mergeableContent = CreateRemappedMergeableContent ( packages , relationships ) ;
99113 return true ;
100114 }
101115
102116 /// <summary>
103117 /// Process packages and return the collection of <see cref="SbomPackage"/> objects.
104118 /// </summary>
105- private IEnumerable < SbomPackage > ProcessPackages ( IEnumerable < SPDXPackage > spdxPackages )
119+ private IList < SbomPackage > ProcessPackages ( IReadOnlyList < SPDXPackage > spdxPackages )
106120 {
107121 var packages = new List < SbomPackage > ( ) ;
108122 foreach ( var spdxPackage in spdxPackages )
109123 {
110- var sbomPackage = new SbomPackage
111- {
112- PackageName = spdxPackage . Name ,
113- PackageVersion = spdxPackage . VersionInfo ,
114- Id = spdxPackage . SpdxId ,
115- FilesAnalyzed = spdxPackage . FilesAnalyzed ,
116- LicenseInfo = new LicenseInfo
117- {
118- Concluded = spdxPackage . LicenseConcluded ,
119- Declared = spdxPackage . LicenseDeclared
120- } ,
121- } ;
124+ var sbomPackage = spdxPackage . ToSbomPackage ( ) ;
122125 packages . Add ( sbomPackage ) ;
123126 }
124127
125128 return packages ;
126129 }
130+
131+ private IList < SbomRelationship > ProcessRelationships ( IReadOnlyList < SPDXRelationship > spdxRelationships )
132+ {
133+ var relationships = new List < SbomRelationship > ( ) ;
134+
135+ foreach ( var spdxRelationship in spdxRelationships )
136+ {
137+ if ( IsDependsOnRelationship ( spdxRelationship ) )
138+ {
139+ var relationship = new SbomRelationship
140+ {
141+ SourceElementId = spdxRelationship . SourceElementId ,
142+ TargetElementId = spdxRelationship . TargetElementId ,
143+ RelationshipType = DependsOn , // Force output consistency
144+ } ;
145+ relationships . Add ( relationship ) ;
146+ }
147+ }
148+
149+ return relationships ;
150+ }
151+
152+ private static bool IsDependsOnRelationship ( SPDXRelationship spdxRelationship )
153+ {
154+ // Include both "DEPENDS_ON" and "DEPENDSON" to cover variations in SPDX files, and ignore case.
155+ return spdxRelationship . RelationshipType . Equals ( "DEPENDS_ON" , StringComparison . InvariantCultureIgnoreCase ) ||
156+ spdxRelationship . RelationshipType . Equals ( "DEPENDSON" , StringComparison . InvariantCultureIgnoreCase ) ;
157+ }
158+
159+ /// <summary>
160+ /// Remaps the root package ID, updates relationships accordingly, then creates a new <see cref="MergeableContent"/> object.
161+ /// </summary>
162+ private MergeableContent CreateRemappedMergeableContent ( IList < SbomPackage > packages , IList < SbomRelationship > relationships )
163+ {
164+ var mappedRootPackageId = GetAdjustedRootPackageId ( packages ) ;
165+
166+ AdjustRootPackageRelationships ( relationships , mappedRootPackageId ) ;
167+
168+ logger . Debug ( $ "MergeableContent includes { packages . Count } package(s) and { relationships . Count } relationship(s).") ;
169+
170+ return new MergeableContent ( packages , relationships ) ;
171+ }
172+
173+ private string GetAdjustedRootPackageId ( IList < SbomPackage > packages )
174+ {
175+ foreach ( var package in packages )
176+ {
177+ if ( package . Id == Constants . RootPackageIdValue )
178+ {
179+ package . Id = null ;
180+ var newSpdxId = CommonSPDXUtils . GenerateSpdxPackageId ( package ) ;
181+ package . Id = newSpdxId ;
182+ logger . Debug ( $ "Remapped root package ID from '{ Constants . RootPackageIdValue } ' to '{ newSpdxId } '") ;
183+ return newSpdxId ;
184+ }
185+ }
186+
187+ throw new InvalidDataException ( "No root package found in the SPDX document." ) ;
188+ }
189+
190+ private void AdjustRootPackageRelationships ( IList < SbomRelationship > relationships , string mappedRootPackageId )
191+ {
192+ // Update relationships where the source is the root package.
193+ foreach ( var relationship in relationships )
194+ {
195+ if ( relationship . SourceElementId == Constants . RootPackageIdValue )
196+ {
197+ // Update the source element ID to the remapped root package ID.
198+ logger . Verbose ( $ "Remapped root package dependency on '{ relationship . TargetElementId } '") ;
199+ relationship . SourceElementId = mappedRootPackageId ;
200+ }
201+ }
202+
203+ // Make the output root depend on the remapped root.
204+ var newRootRelationship = new SbomRelationship
205+ {
206+ SourceElementId = Constants . RootPackageIdValue ,
207+ TargetElementId = mappedRootPackageId ,
208+ RelationshipType = DependsOn , // Force output consistency
209+ } ;
210+ relationships . Add ( newRootRelationship ) ;
211+ logger . Debug ( $ "Added new root relationship from '{ Constants . RootPackageIdValue } ' to '{ mappedRootPackageId } '") ;
212+ }
127213}
0 commit comments