204 lines
6.9 KiB
C#
204 lines
6.9 KiB
C#
// <copyright file="Spdx3VersionDetector.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
|
// </copyright>
|
|
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.Spdx3;
|
|
|
|
/// <summary>
|
|
/// Detects the SPDX version of a document.
|
|
/// </summary>
|
|
public static class Spdx3VersionDetector
|
|
{
|
|
/// <summary>
|
|
/// Detected SPDX version.
|
|
/// </summary>
|
|
public enum SpdxVersion
|
|
{
|
|
/// <summary>
|
|
/// Unknown version.
|
|
/// </summary>
|
|
Unknown,
|
|
|
|
/// <summary>
|
|
/// SPDX 2.2.
|
|
/// </summary>
|
|
Spdx22,
|
|
|
|
/// <summary>
|
|
/// SPDX 2.3.
|
|
/// </summary>
|
|
Spdx23,
|
|
|
|
/// <summary>
|
|
/// SPDX 3.0.1.
|
|
/// </summary>
|
|
Spdx301
|
|
}
|
|
|
|
/// <summary>
|
|
/// Version detection result.
|
|
/// </summary>
|
|
/// <param name="Version">The detected version.</param>
|
|
/// <param name="VersionString">The raw version string if found.</param>
|
|
/// <param name="IsJsonLd">Whether the document uses JSON-LD format.</param>
|
|
public readonly record struct DetectionResult(
|
|
SpdxVersion Version,
|
|
string? VersionString,
|
|
bool IsJsonLd);
|
|
|
|
/// <summary>
|
|
/// Detects the SPDX version from a JSON document.
|
|
/// </summary>
|
|
/// <param name="json">The JSON content.</param>
|
|
/// <returns>The detection result.</returns>
|
|
public static DetectionResult Detect(string json)
|
|
{
|
|
using var document = JsonDocument.Parse(json);
|
|
return Detect(document.RootElement);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects the SPDX version from a JSON element.
|
|
/// </summary>
|
|
/// <param name="root">The root JSON element.</param>
|
|
/// <returns>The detection result.</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects the SPDX version from a stream.
|
|
/// </summary>
|
|
/// <param name="stream">The input stream.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The detection result.</returns>
|
|
public static async Task<DetectionResult> DetectAsync(
|
|
Stream stream,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken)
|
|
.ConfigureAwait(false);
|
|
return Detect(document.RootElement);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the recommended parser for the detected version.
|
|
/// </summary>
|
|
/// <param name="version">The detected version.</param>
|
|
/// <returns>Parser recommendation.</returns>
|
|
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;
|
|
}
|
|
}
|