// // Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later. // using System.Collections.Immutable; using System.Text.Json; using Microsoft.Extensions.Logging; using StellaOps.Spdx3.JsonLd; using StellaOps.Spdx3.Model; using StellaOps.Spdx3.Model.Software; namespace StellaOps.Spdx3; /// /// SPDX 3.0.1 JSON-LD parser implementation. /// public sealed class Spdx3Parser : ISpdx3Parser { private readonly ISpdx3ContextResolver _contextResolver; private readonly ILogger _logger; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; /// /// Initializes a new instance of the class. /// public Spdx3Parser( ISpdx3ContextResolver contextResolver, ILogger logger) { _contextResolver = contextResolver; _logger = logger; } /// public async Task ParseAsync( Stream stream, CancellationToken cancellationToken = default) { try { using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken) .ConfigureAwait(false); return await ParseDocumentAsync(document, cancellationToken) .ConfigureAwait(false); } catch (JsonException ex) { _logger.LogError(ex, "Failed to parse SPDX 3.0.1 JSON"); return Spdx3ParseResult.Failed("JSON_PARSE_ERROR", ex.Message, ex.Path); } } /// public async Task ParseAsync( string filePath, CancellationToken cancellationToken = default) { if (!File.Exists(filePath)) { return Spdx3ParseResult.Failed("FILE_NOT_FOUND", $"File not found: {filePath}"); } await using var stream = File.OpenRead(filePath); return await ParseAsync(stream, cancellationToken).ConfigureAwait(false); } /// public async Task ParseFromJsonAsync( string json, CancellationToken cancellationToken = default) { using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json)); return await ParseAsync(stream, cancellationToken).ConfigureAwait(false); } private async Task ParseDocumentAsync( JsonDocument document, CancellationToken cancellationToken) { var root = document.RootElement; var errors = new List(); var warnings = new List(); // Check for @context (JSON-LD indicator) if (!root.TryGetProperty("@context", out var contextElement)) { return Spdx3ParseResult.Failed("MISSING_CONTEXT", "No @context found. Is this an SPDX 3.0.1 JSON-LD document?"); } // Resolve context (for future expansion support) var contextRef = GetContextReference(contextElement); if (!string.IsNullOrEmpty(contextRef)) { await _contextResolver.ResolveAsync(contextRef, cancellationToken).ConfigureAwait(false); } // Parse @graph (main element array) var elements = new List(); var creationInfos = new List(); var profiles = new HashSet(); Spdx3SpdxDocument? spdxDocument = null; if (root.TryGetProperty("@graph", out var graphElement) && graphElement.ValueKind == JsonValueKind.Array) { foreach (var element in graphElement.EnumerateArray()) { var parsed = ParseElement(element, errors, warnings); if (parsed != null) { elements.Add(parsed); if (parsed is Spdx3SpdxDocument doc) { spdxDocument = doc; } } // Also extract CreationInfo var creationInfo = ExtractCreationInfo(element, errors); if (creationInfo != null) { creationInfos.Add(creationInfo); foreach (var profile in creationInfo.Profile) { profiles.Add(profile); } } } } else { // Single document format (no @graph) var parsed = ParseElement(root, errors, warnings); if (parsed != null) { elements.Add(parsed); if (parsed is Spdx3SpdxDocument doc) { spdxDocument = doc; } } } if (errors.Count > 0) { return Spdx3ParseResult.Failed(errors, warnings); } var result = new Spdx3Document(elements, creationInfos, profiles, spdxDocument); return Spdx3ParseResult.Succeeded(result, warnings); } private Spdx3Element? ParseElement( JsonElement element, List errors, List warnings) { if (element.ValueKind != JsonValueKind.Object) { return null; } // Get @type to determine element type var type = GetStringProperty(element, "@type") ?? GetStringProperty(element, "type"); if (string.IsNullOrEmpty(type)) { return null; } // Get spdxId (required) var spdxId = GetStringProperty(element, "spdxId") ?? GetStringProperty(element, "@id"); if (string.IsNullOrEmpty(spdxId)) { warnings.Add(new Spdx3ParseWarning("MISSING_SPDX_ID", $"Element of type {type} has no spdxId")); return null; } return type switch { "software_Package" or "Package" or "spdx:software_Package" => ParsePackage(element, spdxId), "software_File" or "File" or "spdx:software_File" => ParseFile(element, spdxId), "software_Snippet" or "Snippet" or "spdx:software_Snippet" => ParseSnippet(element, spdxId), "software_SpdxDocument" or "SpdxDocument" or "spdx:software_SpdxDocument" => ParseSpdxDocument(element, spdxId), "Relationship" or "spdx:Relationship" => ParseRelationship(element, spdxId), "Person" or "spdx:Person" => ParseAgent(element, spdxId), "Organization" or "spdx:Organization" => ParseAgent(element, spdxId), "Tool" or "spdx:Tool" => ParseAgent(element, spdxId), _ => ParseGenericElement(element, spdxId, type, warnings) }; } private Spdx3Package ParsePackage(JsonElement element, string spdxId) { return new Spdx3Package { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name"), Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), Comment = GetStringProperty(element, "comment"), PackageVersion = GetStringProperty(element, "packageVersion"), DownloadLocation = GetStringProperty(element, "downloadLocation"), PackageUrl = GetStringProperty(element, "packageUrl"), HomePage = GetStringProperty(element, "homePage"), SourceInfo = GetStringProperty(element, "sourceInfo"), CopyrightText = GetStringProperty(element, "copyrightText"), SuppliedBy = GetStringProperty(element, "suppliedBy"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element), OriginatedBy = GetStringArrayProperty(element, "originatedBy"), AttributionText = GetStringArrayProperty(element, "attributionText") }; } private Spdx3File ParseFile(JsonElement element, string spdxId) { return new Spdx3File { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name") ?? string.Empty, Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), Comment = GetStringProperty(element, "comment"), ContentType = GetStringProperty(element, "contentType"), CopyrightText = GetStringProperty(element, "copyrightText"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element) }; } private Spdx3Snippet ParseSnippet(JsonElement element, string spdxId) { return new Spdx3Snippet { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name"), Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), Comment = GetStringProperty(element, "comment"), SnippetFromFile = GetStringProperty(element, "snippetFromFile") ?? string.Empty, CopyrightText = GetStringProperty(element, "copyrightText"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element) }; } private Spdx3SpdxDocument ParseSpdxDocument(JsonElement element, string spdxId) { return new Spdx3SpdxDocument { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = GetStringProperty(element, "name"), Summary = GetStringProperty(element, "summary"), Description = GetStringProperty(element, "description"), Comment = GetStringProperty(element, "comment"), Element = GetStringArrayProperty(element, "element"), RootElement = GetStringArrayProperty(element, "rootElement"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element) }; } private Spdx3Relationship ParseRelationship(JsonElement element, string spdxId) { var toValue = GetStringProperty(element, "to"); var toArray = toValue != null ? [toValue] : GetStringArrayProperty(element, "to"); var relationshipType = GetStringProperty(element, "relationshipType") ?? "Other"; if (!Enum.TryParse(relationshipType, ignoreCase: true, out var relType)) { relType = Spdx3RelationshipType.Other; } return new Spdx3Relationship { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), From = GetStringProperty(element, "from") ?? string.Empty, To = toArray, RelationshipType = relType, Comment = GetStringProperty(element, "comment"), VerifiedUsing = ParseIntegrityMethods(element), ExternalRef = ParseExternalRefs(element), ExternalIdentifier = ParseExternalIdentifiers(element) }; } private T ParseAgent(JsonElement element, string spdxId) where T : Spdx3Element { var name = GetStringProperty(element, "name") ?? string.Empty; if (typeof(T) == typeof(Spdx3Person)) { return (T)(object)new Spdx3Person { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = name }; } if (typeof(T) == typeof(Spdx3Organization)) { return (T)(object)new Spdx3Organization { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = name }; } if (typeof(T) == typeof(Spdx3Tool)) { return (T)(object)new Spdx3Tool { SpdxId = spdxId, Type = GetStringProperty(element, "@type"), Name = name }; } throw new InvalidOperationException($"Unsupported agent type: {typeof(T)}"); } private Spdx3Element? ParseGenericElement( JsonElement element, string spdxId, string type, List warnings) { // For unknown types, we could create a generic element // For now, log a warning and skip warnings.Add(new Spdx3ParseWarning("UNKNOWN_TYPE", $"Unknown element type: {type}", spdxId)); return null; } private Spdx3CreationInfo? ExtractCreationInfo( JsonElement element, List errors) { if (!element.TryGetProperty("creationInfo", out var ciElement)) { return null; } if (ciElement.ValueKind == JsonValueKind.String) { // Reference to CreationInfo - will be resolved later return null; } if (ciElement.ValueKind != JsonValueKind.Object) { return null; } var specVersion = GetStringProperty(ciElement, "specVersion"); if (string.IsNullOrEmpty(specVersion)) { return null; } var createdStr = GetStringProperty(ciElement, "created"); if (!DateTimeOffset.TryParse(createdStr, out var created)) { created = DateTimeOffset.UtcNow; } var profileStrings = GetStringArrayProperty(ciElement, "profile"); var profiles = profileStrings .Select(p => Spdx3ProfileUris.Parse(p)) .Where(p => p.HasValue) .Select(p => p!.Value) .ToImmutableArray(); return new Spdx3CreationInfo { Id = GetStringProperty(ciElement, "@id"), Type = GetStringProperty(ciElement, "@type"), SpecVersion = specVersion, Created = created, CreatedBy = GetStringArrayProperty(ciElement, "createdBy"), CreatedUsing = GetStringArrayProperty(ciElement, "createdUsing"), Profile = profiles, DataLicense = GetStringProperty(ciElement, "dataLicense"), Comment = GetStringProperty(ciElement, "comment") }; } private ImmutableArray ParseIntegrityMethods(JsonElement element) { if (!element.TryGetProperty("verifiedUsing", out var verifiedUsing)) { return []; } if (verifiedUsing.ValueKind != JsonValueKind.Array) { return []; } var methods = new List(); foreach (var item in verifiedUsing.EnumerateArray()) { var algorithm = GetStringProperty(item, "algorithm"); var hashValue = GetStringProperty(item, "hashValue"); if (!string.IsNullOrEmpty(algorithm) && !string.IsNullOrEmpty(hashValue)) { if (Enum.TryParse(algorithm.Replace("-", "_"), ignoreCase: true, out var algo)) { methods.Add(new Spdx3Hash { Type = GetStringProperty(item, "@type"), Algorithm = algo, HashValue = hashValue.ToLowerInvariant(), Comment = GetStringProperty(item, "comment") }); } } } return [.. methods]; } private ImmutableArray ParseExternalRefs(JsonElement element) { if (!element.TryGetProperty("externalRef", out var externalRef)) { return []; } if (externalRef.ValueKind != JsonValueKind.Array) { return []; } var refs = new List(); foreach (var item in externalRef.EnumerateArray()) { refs.Add(new Spdx3ExternalRef { Type = GetStringProperty(item, "@type"), Locator = GetStringArrayProperty(item, "locator"), ContentType = GetStringProperty(item, "contentType"), Comment = GetStringProperty(item, "comment") }); } return [.. refs]; } private ImmutableArray ParseExternalIdentifiers(JsonElement element) { if (!element.TryGetProperty("externalIdentifier", out var externalId)) { return []; } if (externalId.ValueKind != JsonValueKind.Array) { return []; } var identifiers = new List(); foreach (var item in externalId.EnumerateArray()) { var identifier = GetStringProperty(item, "identifier"); if (string.IsNullOrEmpty(identifier)) { continue; } var typeStr = GetStringProperty(item, "externalIdentifierType"); Spdx3ExternalIdentifierType? idType = null; if (!string.IsNullOrEmpty(typeStr) && Enum.TryParse(typeStr, ignoreCase: true, out var parsed)) { idType = parsed; } identifiers.Add(new Spdx3ExternalIdentifier { Type = GetStringProperty(item, "@type"), ExternalIdentifierType = idType, Identifier = identifier, Comment = GetStringProperty(item, "comment"), IssuingAuthority = GetStringProperty(item, "issuingAuthority") }); } return [.. identifiers]; } private static string? GetStringProperty(JsonElement element, string propertyName) { if (element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String) { return prop.GetString(); } return null; } private static ImmutableArray GetStringArrayProperty(JsonElement element, string propertyName) { if (!element.TryGetProperty(propertyName, out var prop)) { return []; } if (prop.ValueKind == JsonValueKind.String) { var value = prop.GetString(); return value != null ? [value] : []; } if (prop.ValueKind == JsonValueKind.Array) { var list = new List(); foreach (var item in prop.EnumerateArray()) { if (item.ValueKind == JsonValueKind.String) { var value = item.GetString(); if (value != null) { list.Add(value); } } } return [.. list]; } return []; } private static string? GetContextReference(JsonElement contextElement) { if (contextElement.ValueKind == JsonValueKind.String) { return contextElement.GetString(); } if (contextElement.ValueKind == JsonValueKind.Array) { foreach (var item in contextElement.EnumerateArray()) { if (item.ValueKind == JsonValueKind.String) { return item.GetString(); } } } return null; } }