audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
587
src/__Libraries/StellaOps.Spdx3/Spdx3Parser.cs
Normal file
587
src/__Libraries/StellaOps.Spdx3/Spdx3Parser.cs
Normal file
@@ -0,0 +1,587 @@
|
||||
// <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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user