Files
git.stella-ops.org/src/__Libraries/StellaOps.Spdx3/Model/Spdx3Document.cs
2026-02-01 21:37:40 +02:00

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));
}
}