// // Copyright (c) StellaOps. Licensed under the BUSL-1.1. // using System.Text.Json; namespace StellaOps.Spdx3; /// /// Detects the SPDX version of a document. /// public static class Spdx3VersionDetector { /// /// Detected SPDX version. /// public enum SpdxVersion { /// /// Unknown version. /// Unknown, /// /// SPDX 2.2. /// Spdx22, /// /// SPDX 2.3. /// Spdx23, /// /// SPDX 3.0.1. /// Spdx301 } /// /// Version detection result. /// /// The detected version. /// The raw version string if found. /// Whether the document uses JSON-LD format. public readonly record struct DetectionResult( SpdxVersion Version, string? VersionString, bool IsJsonLd); /// /// Detects the SPDX version from a JSON document. /// /// The JSON content. /// The detection result. public static DetectionResult Detect(string json) { using var document = JsonDocument.Parse(json); return Detect(document.RootElement); } /// /// Detects the SPDX version from a JSON element. /// /// The root JSON element. /// The detection result. public static DetectionResult Detect(JsonElement root) { // Check for JSON-LD @context (SPDX 3.x indicator) if (root.TryGetProperty("@context", out var context)) { var contextStr = GetContextString(context); if (!string.IsNullOrEmpty(contextStr)) { // Check for specific 3.0.1 context if (contextStr.Contains("3.0.1", StringComparison.OrdinalIgnoreCase) || contextStr.Contains("spdx.org/rdf/3", StringComparison.OrdinalIgnoreCase)) { return new DetectionResult(SpdxVersion.Spdx301, "3.0.1", true); } // Generic 3.x detection if (contextStr.Contains("spdx.org/rdf", StringComparison.OrdinalIgnoreCase)) { return new DetectionResult(SpdxVersion.Spdx301, null, true); } } // Has @context but couldn't determine specific version return new DetectionResult(SpdxVersion.Spdx301, null, true); } // Check for SPDX 2.x spdxVersion field if (root.TryGetProperty("spdxVersion", out var spdxVersion) && spdxVersion.ValueKind == JsonValueKind.String) { var versionStr = spdxVersion.GetString(); if (!string.IsNullOrEmpty(versionStr)) { if (versionStr.Contains("2.3", StringComparison.OrdinalIgnoreCase)) { return new DetectionResult(SpdxVersion.Spdx23, versionStr, false); } if (versionStr.Contains("2.2", StringComparison.OrdinalIgnoreCase)) { return new DetectionResult(SpdxVersion.Spdx22, versionStr, false); } // Older 2.x versions if (versionStr.StartsWith("SPDX-2", StringComparison.OrdinalIgnoreCase)) { return new DetectionResult(SpdxVersion.Spdx22, versionStr, false); } } } // Check for creationInfo.specVersion (SPDX 3.x in @graph format) if (root.TryGetProperty("@graph", out var graph) && graph.ValueKind == JsonValueKind.Array) { foreach (var element in graph.EnumerateArray()) { if (element.TryGetProperty("creationInfo", out var creationInfo) && creationInfo.ValueKind == JsonValueKind.Object) { if (creationInfo.TryGetProperty("specVersion", out var specVersion) && specVersion.ValueKind == JsonValueKind.String) { var specVersionStr = specVersion.GetString(); if (specVersionStr == "3.0.1") { return new DetectionResult(SpdxVersion.Spdx301, specVersionStr, true); } } } } } return new DetectionResult(SpdxVersion.Unknown, null, false); } /// /// Detects the SPDX version from a stream. /// /// The input stream. /// Cancellation token. /// The detection result. public static async Task DetectAsync( Stream stream, CancellationToken cancellationToken = default) { using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken) .ConfigureAwait(false); return Detect(document.RootElement); } /// /// Gets the recommended parser for the detected version. /// /// The detected version. /// Parser recommendation. public static string GetParserRecommendation(SpdxVersion version) => version switch { SpdxVersion.Spdx22 => "Use SpdxParser (SPDX 2.x parser)", SpdxVersion.Spdx23 => "Use SpdxParser (SPDX 2.x parser)", SpdxVersion.Spdx301 => "Use Spdx3Parser (SPDX 3.0.1 parser)", _ => "Unknown format - manual inspection required" }; private static string? GetContextString(JsonElement context) { if (context.ValueKind == JsonValueKind.String) { return context.GetString(); } if (context.ValueKind == JsonValueKind.Array) { foreach (var item in context.EnumerateArray()) { if (item.ValueKind == JsonValueKind.String) { var str = item.GetString(); if (!string.IsNullOrEmpty(str) && str.Contains("spdx", StringComparison.OrdinalIgnoreCase)) { return str; } } } // Return first string if no spdx-specific one found foreach (var item in context.EnumerateArray()) { if (item.ValueKind == JsonValueKind.String) { return item.GetString(); } } } return null; } }