// // Copyright (c) StellaOps. Licensed under the BUSL-1.1. // using StellaOps.Spdx3.Model.Software; using System.Collections.Immutable; namespace StellaOps.Spdx3.Model; /// /// Represents a parsed SPDX 3.0.1 document containing all elements. /// public sealed class Spdx3Document { private readonly Dictionary _elementsById; private readonly Dictionary _creationInfoById; /// /// Initializes a new instance of the class. /// /// All elements in the document. /// All CreationInfo objects. /// Detected profile conformance. /// The root SpdxDocument element if present. public Spdx3Document( IEnumerable elements, IEnumerable creationInfos, IEnumerable 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().ToImmutableArray(); Files = elementList.OfType().ToImmutableArray(); Snippets = elementList.OfType().ToImmutableArray(); Relationships = elementList.OfType().ToImmutableArray(); AllElements = elementList.ToImmutableArray(); } /// /// Gets all elements including duplicates (for validation). /// public ImmutableArray AllElements { get; } /// /// Gets the root SpdxDocument element if present. /// public Spdx3SpdxDocument? SpdxDocument { get; } /// /// Gets all elements in the document. /// public IReadOnlyCollection Elements => _elementsById.Values; /// /// Gets all packages in the document. /// public ImmutableArray Packages { get; } /// /// Gets all files in the document. /// public ImmutableArray Files { get; } /// /// Gets all snippets in the document. /// public ImmutableArray Snippets { get; } /// /// Gets all relationships in the document. /// public ImmutableArray Relationships { get; } /// /// Gets the detected profile conformance. /// public ImmutableHashSet Profiles { get; } /// /// Gets all creation info objects in the document. /// public IReadOnlyCollection CreationInfos => _creationInfoById.Values; /// /// Gets an element by its SPDX ID. /// /// The SPDX ID. /// The element, or null if not found. public Spdx3Element? GetById(string spdxId) { return _elementsById.TryGetValue(spdxId, out var element) ? element : null; } /// /// Gets an element by its SPDX ID as a specific type. /// /// The element type. /// The SPDX ID. /// The element, or null if not found or wrong type. public T? GetById(string spdxId) where T : Spdx3Element { return GetById(spdxId) as T; } /// /// Gets a CreationInfo by its ID. /// /// The CreationInfo ID. /// The CreationInfo, or null if not found. public Spdx3CreationInfo? GetCreationInfo(string id) { return _creationInfoById.TryGetValue(id, out var info) ? info : null; } /// /// Gets relationships where the given element is the source. /// /// The source element ID. /// Matching relationships. public IEnumerable GetRelationshipsFrom(string spdxId) { return Relationships.Where(r => r.From == spdxId); } /// /// Gets relationships where the given element is a target. /// /// The target element ID. /// Matching relationships. public IEnumerable GetRelationshipsTo(string spdxId) { return Relationships.Where(r => r.To.Contains(spdxId)); } /// /// Gets direct dependencies of a package. /// /// The package SPDX ID. /// Dependent packages. public IEnumerable GetDependencies(string packageId) { return GetRelationshipsFrom(packageId) .Where(r => r.RelationshipType == Spdx3RelationshipType.DependsOn) .SelectMany(r => r.To) .Select(GetById) .Where(p => p != null) .Cast(); } /// /// Gets all files contained in a package. /// /// The package SPDX ID. /// Contained files. public IEnumerable GetContainedFiles(string packageId) { return GetRelationshipsFrom(packageId) .Where(r => r.RelationshipType == Spdx3RelationshipType.Contains) .SelectMany(r => r.To) .Select(GetById) .Where(f => f != null) .Cast(); } /// /// Checks if the document conforms to a specific profile. /// /// The profile to check. /// True if the document conforms. public bool ConformsTo(Spdx3ProfileIdentifier profile) { return Profiles.Contains(profile); } /// /// Gets all PURLs from packages in the document. /// /// Package URLs. public IEnumerable GetAllPurls() { return Packages .SelectMany(p => p.ExternalIdentifier) .Where(i => i.ExternalIdentifierType == Spdx3ExternalIdentifierType.PackageUrl) .Select(i => i.Identifier) .Distinct(StringComparer.Ordinal); } /// /// Gets the root package (if any). /// /// The root package, or null. public Spdx3Package? GetRootPackage() { if (SpdxDocument?.RootElement.Length > 0) { var rootId = SpdxDocument.RootElement[0]; return GetById(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)); } }