//
// 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;
}
}