588 lines
20 KiB
C#
588 lines
20 KiB
C#
// <copyright file="Spdx3Parser.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
|
// </copyright>
|
|
|
|
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;
|
|
|
|
/// <summary>
|
|
/// SPDX 3.0.1 JSON-LD parser implementation.
|
|
/// </summary>
|
|
public sealed class Spdx3Parser : ISpdx3Parser
|
|
{
|
|
private readonly ISpdx3ContextResolver _contextResolver;
|
|
private readonly ILogger<Spdx3Parser> _logger;
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
|
AllowTrailingCommas = true
|
|
};
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Spdx3Parser"/> class.
|
|
/// </summary>
|
|
public Spdx3Parser(
|
|
ISpdx3ContextResolver contextResolver,
|
|
ILogger<Spdx3Parser> logger)
|
|
{
|
|
_contextResolver = contextResolver;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Spdx3ParseResult> 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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Spdx3ParseResult> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Spdx3ParseResult> 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<Spdx3ParseResult> ParseDocumentAsync(
|
|
JsonDocument document,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var root = document.RootElement;
|
|
var errors = new List<Spdx3ParseError>();
|
|
var warnings = new List<Spdx3ParseWarning>();
|
|
|
|
// 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<Spdx3Element>();
|
|
var creationInfos = new List<Spdx3CreationInfo>();
|
|
var profiles = new HashSet<Spdx3ProfileIdentifier>();
|
|
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<Spdx3ParseError> errors,
|
|
List<Spdx3ParseWarning> 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<Spdx3Person>(element, spdxId),
|
|
"Organization" or "spdx:Organization" =>
|
|
ParseAgent<Spdx3Organization>(element, spdxId),
|
|
"Tool" or "spdx:Tool" =>
|
|
ParseAgent<Spdx3Tool>(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<Spdx3RelationshipType>(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<T>(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<Spdx3ParseWarning> 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<Spdx3ParseError> 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<Spdx3IntegrityMethod> ParseIntegrityMethods(JsonElement element)
|
|
{
|
|
if (!element.TryGetProperty("verifiedUsing", out var verifiedUsing))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
if (verifiedUsing.ValueKind != JsonValueKind.Array)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var methods = new List<Spdx3IntegrityMethod>();
|
|
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<Spdx3HashAlgorithm>(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<Spdx3ExternalRef> ParseExternalRefs(JsonElement element)
|
|
{
|
|
if (!element.TryGetProperty("externalRef", out var externalRef))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
if (externalRef.ValueKind != JsonValueKind.Array)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var refs = new List<Spdx3ExternalRef>();
|
|
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<Spdx3ExternalIdentifier> ParseExternalIdentifiers(JsonElement element)
|
|
{
|
|
if (!element.TryGetProperty("externalIdentifier", out var externalId))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
if (externalId.ValueKind != JsonValueKind.Array)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var identifiers = new List<Spdx3ExternalIdentifier>();
|
|
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<Spdx3ExternalIdentifierType>(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<string> 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<string>();
|
|
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;
|
|
}
|
|
}
|