225 lines
7.8 KiB
C#
225 lines
7.8 KiB
C#
// <copyright file="Spdx3Document.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
|
// </copyright>
|
|
|
|
|
|
using StellaOps.Spdx3.Model.Software;
|
|
using System.Collections.Immutable;
|
|
|
|
namespace StellaOps.Spdx3.Model;
|
|
|
|
/// <summary>
|
|
/// Represents a parsed SPDX 3.0.1 document containing all elements.
|
|
/// </summary>
|
|
public sealed class Spdx3Document
|
|
{
|
|
private readonly Dictionary<string, Spdx3Element> _elementsById;
|
|
private readonly Dictionary<string, Spdx3CreationInfo> _creationInfoById;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Spdx3Document"/> class.
|
|
/// </summary>
|
|
/// <param name="elements">All elements in the document.</param>
|
|
/// <param name="creationInfos">All CreationInfo objects.</param>
|
|
/// <param name="profiles">Detected profile conformance.</param>
|
|
/// <param name="spdxDocument">The root SpdxDocument element if present.</param>
|
|
public Spdx3Document(
|
|
IEnumerable<Spdx3Element> elements,
|
|
IEnumerable<Spdx3CreationInfo> creationInfos,
|
|
IEnumerable<Spdx3ProfileIdentifier> profiles,
|
|
Spdx3SpdxDocument? spdxDocument = null)
|
|
{
|
|
var elementList = elements.ToList();
|
|
|
|
// Use GroupBy to handle duplicates - last element wins for lookup but keeps all for validation
|
|
_elementsById = elementList
|
|
.GroupBy(e => e.SpdxId, StringComparer.Ordinal)
|
|
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.Ordinal);
|
|
|
|
_creationInfoById = creationInfos
|
|
.Where(c => c.Id != null)
|
|
.GroupBy(c => c.Id!, StringComparer.Ordinal)
|
|
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.Ordinal);
|
|
|
|
Profiles = profiles.ToImmutableHashSet();
|
|
SpdxDocument = spdxDocument;
|
|
|
|
// Categorize elements by type - use original list to preserve duplicates for counting
|
|
Packages = elementList.OfType<Spdx3Package>().ToImmutableArray();
|
|
Files = elementList.OfType<Spdx3File>().ToImmutableArray();
|
|
Snippets = elementList.OfType<Spdx3Snippet>().ToImmutableArray();
|
|
Relationships = elementList.OfType<Spdx3Relationship>().ToImmutableArray();
|
|
AllElements = elementList.ToImmutableArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all elements including duplicates (for validation).
|
|
/// </summary>
|
|
public ImmutableArray<Spdx3Element> AllElements { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the root SpdxDocument element if present.
|
|
/// </summary>
|
|
public Spdx3SpdxDocument? SpdxDocument { get; }
|
|
|
|
/// <summary>
|
|
/// Gets all elements in the document.
|
|
/// </summary>
|
|
public IReadOnlyCollection<Spdx3Element> Elements => _elementsById.Values;
|
|
|
|
/// <summary>
|
|
/// Gets all packages in the document.
|
|
/// </summary>
|
|
public ImmutableArray<Spdx3Package> Packages { get; }
|
|
|
|
/// <summary>
|
|
/// Gets all files in the document.
|
|
/// </summary>
|
|
public ImmutableArray<Spdx3File> Files { get; }
|
|
|
|
/// <summary>
|
|
/// Gets all snippets in the document.
|
|
/// </summary>
|
|
public ImmutableArray<Spdx3Snippet> Snippets { get; }
|
|
|
|
/// <summary>
|
|
/// Gets all relationships in the document.
|
|
/// </summary>
|
|
public ImmutableArray<Spdx3Relationship> Relationships { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the detected profile conformance.
|
|
/// </summary>
|
|
public ImmutableHashSet<Spdx3ProfileIdentifier> Profiles { get; }
|
|
|
|
/// <summary>
|
|
/// Gets all creation info objects in the document.
|
|
/// </summary>
|
|
public IReadOnlyCollection<Spdx3CreationInfo> CreationInfos => _creationInfoById.Values;
|
|
|
|
/// <summary>
|
|
/// Gets an element by its SPDX ID.
|
|
/// </summary>
|
|
/// <param name="spdxId">The SPDX ID.</param>
|
|
/// <returns>The element, or null if not found.</returns>
|
|
public Spdx3Element? GetById(string spdxId)
|
|
{
|
|
return _elementsById.TryGetValue(spdxId, out var element) ? element : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an element by its SPDX ID as a specific type.
|
|
/// </summary>
|
|
/// <typeparam name="T">The element type.</typeparam>
|
|
/// <param name="spdxId">The SPDX ID.</param>
|
|
/// <returns>The element, or null if not found or wrong type.</returns>
|
|
public T? GetById<T>(string spdxId) where T : Spdx3Element
|
|
{
|
|
return GetById(spdxId) as T;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a CreationInfo by its ID.
|
|
/// </summary>
|
|
/// <param name="id">The CreationInfo ID.</param>
|
|
/// <returns>The CreationInfo, or null if not found.</returns>
|
|
public Spdx3CreationInfo? GetCreationInfo(string id)
|
|
{
|
|
return _creationInfoById.TryGetValue(id, out var info) ? info : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets relationships where the given element is the source.
|
|
/// </summary>
|
|
/// <param name="spdxId">The source element ID.</param>
|
|
/// <returns>Matching relationships.</returns>
|
|
public IEnumerable<Spdx3Relationship> GetRelationshipsFrom(string spdxId)
|
|
{
|
|
return Relationships.Where(r => r.From == spdxId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets relationships where the given element is a target.
|
|
/// </summary>
|
|
/// <param name="spdxId">The target element ID.</param>
|
|
/// <returns>Matching relationships.</returns>
|
|
public IEnumerable<Spdx3Relationship> GetRelationshipsTo(string spdxId)
|
|
{
|
|
return Relationships.Where(r => r.To.Contains(spdxId));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets direct dependencies of a package.
|
|
/// </summary>
|
|
/// <param name="packageId">The package SPDX ID.</param>
|
|
/// <returns>Dependent packages.</returns>
|
|
public IEnumerable<Spdx3Package> GetDependencies(string packageId)
|
|
{
|
|
return GetRelationshipsFrom(packageId)
|
|
.Where(r => r.RelationshipType == Spdx3RelationshipType.DependsOn)
|
|
.SelectMany(r => r.To)
|
|
.Select(GetById<Spdx3Package>)
|
|
.Where(p => p != null)
|
|
.Cast<Spdx3Package>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all files contained in a package.
|
|
/// </summary>
|
|
/// <param name="packageId">The package SPDX ID.</param>
|
|
/// <returns>Contained files.</returns>
|
|
public IEnumerable<Spdx3File> GetContainedFiles(string packageId)
|
|
{
|
|
return GetRelationshipsFrom(packageId)
|
|
.Where(r => r.RelationshipType == Spdx3RelationshipType.Contains)
|
|
.SelectMany(r => r.To)
|
|
.Select(GetById<Spdx3File>)
|
|
.Where(f => f != null)
|
|
.Cast<Spdx3File>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the document conforms to a specific profile.
|
|
/// </summary>
|
|
/// <param name="profile">The profile to check.</param>
|
|
/// <returns>True if the document conforms.</returns>
|
|
public bool ConformsTo(Spdx3ProfileIdentifier profile)
|
|
{
|
|
return Profiles.Contains(profile);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all PURLs from packages in the document.
|
|
/// </summary>
|
|
/// <returns>Package URLs.</returns>
|
|
public IEnumerable<string> GetAllPurls()
|
|
{
|
|
return Packages
|
|
.SelectMany(p => p.ExternalIdentifier)
|
|
.Where(i => i.ExternalIdentifierType == Spdx3ExternalIdentifierType.PackageUrl)
|
|
.Select(i => i.Identifier)
|
|
.Distinct(StringComparer.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the root package (if any).
|
|
/// </summary>
|
|
/// <returns>The root package, or null.</returns>
|
|
public Spdx3Package? GetRootPackage()
|
|
{
|
|
if (SpdxDocument?.RootElement.Length > 0)
|
|
{
|
|
var rootId = SpdxDocument.RootElement[0];
|
|
return GetById<Spdx3Package>(rootId);
|
|
}
|
|
|
|
// Fallback: find package with no incoming Contains relationships
|
|
var containedIds = Relationships
|
|
.Where(r => r.RelationshipType == Spdx3RelationshipType.Contains)
|
|
.SelectMany(r => r.To)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
return Packages.FirstOrDefault(p => !containedIds.Contains(p.SpdxId));
|
|
}
|
|
}
|