audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
247
src/__Libraries/StellaOps.Spdx3/JsonLd/Spdx3ContextResolver.cs
Normal file
247
src/__Libraries/StellaOps.Spdx3/JsonLd/Spdx3ContextResolver.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
// <copyright file="Spdx3ContextResolver.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Spdx3.JsonLd;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves JSON-LD contexts for SPDX 3.0.1 documents.
|
||||
/// </summary>
|
||||
public interface ISpdx3ContextResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a context from a URL or embedded reference.
|
||||
/// </summary>
|
||||
/// <param name="contextRef">The context reference (URL or inline).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The resolved context as a JSON element.</returns>
|
||||
Task<Spdx3Context?> ResolveAsync(
|
||||
string contextRef,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a resolved JSON-LD context.
|
||||
/// </summary>
|
||||
public sealed record Spdx3Context
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the context URI.
|
||||
/// </summary>
|
||||
public required string Uri { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the context document.
|
||||
/// </summary>
|
||||
public required JsonDocument Document { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets when the context was resolved.
|
||||
/// </summary>
|
||||
public required DateTimeOffset ResolvedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for the SPDX 3 context resolver.
|
||||
/// </summary>
|
||||
public sealed class Spdx3ContextResolverOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the cache TTL for resolved contexts.
|
||||
/// </summary>
|
||||
public TimeSpan CacheTtl { get; set; } = TimeSpan.FromHours(24);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum cache size.
|
||||
/// </summary>
|
||||
public int MaxCacheSize { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to allow remote context resolution.
|
||||
/// Set to false for air-gapped environments.
|
||||
/// </summary>
|
||||
public bool AllowRemoteContexts { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base path for local context files.
|
||||
/// </summary>
|
||||
public string? LocalContextPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP timeout for remote contexts.
|
||||
/// </summary>
|
||||
public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of context resolver with caching.
|
||||
/// </summary>
|
||||
public sealed class Spdx3ContextResolver : ISpdx3ContextResolver, IDisposable
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ILogger<Spdx3ContextResolver> _logger;
|
||||
private readonly Spdx3ContextResolverOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly Dictionary<string, string> EmbeddedContexts = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["https://spdx.org/rdf/3.0.1/spdx-context.jsonld"] = GetEmbeddedContext("spdx-context.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Core"] = GetEmbeddedContext("core-profile.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Software"] = GetEmbeddedContext("software-profile.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Security"] = GetEmbeddedContext("security-profile.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Build"] = GetEmbeddedContext("build-profile.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Lite"] = GetEmbeddedContext("lite-profile.jsonld")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Spdx3ContextResolver"/> class.
|
||||
/// </summary>
|
||||
public Spdx3ContextResolver(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IMemoryCache cache,
|
||||
ILogger<Spdx3ContextResolver> logger,
|
||||
IOptions<Spdx3ContextResolverOptions> options,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Spdx3Context?> ResolveAsync(
|
||||
string contextRef,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contextRef))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
var cacheKey = $"spdx3-context:{contextRef}";
|
||||
if (_cache.TryGetValue(cacheKey, out Spdx3Context? cached))
|
||||
{
|
||||
_logger.LogDebug("Context cache hit for {ContextRef}", contextRef);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Try embedded contexts first (for air-gap support)
|
||||
if (EmbeddedContexts.TryGetValue(contextRef, out var embedded))
|
||||
{
|
||||
var context = CreateContext(contextRef, embedded);
|
||||
CacheContext(cacheKey, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
// Try local file if configured
|
||||
if (!string.IsNullOrEmpty(_options.LocalContextPath))
|
||||
{
|
||||
var localPath = Path.Combine(_options.LocalContextPath, GetContextFileName(contextRef));
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(localPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var context = CreateContext(contextRef, content);
|
||||
CacheContext(cacheKey, context);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch remote if allowed
|
||||
if (!_options.AllowRemoteContexts)
|
||||
{
|
||||
_logger.LogWarning("Remote context resolution disabled, cannot resolve {ContextRef}", contextRef);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await FetchRemoteContextAsync(contextRef, cacheKey, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<Spdx3Context?> FetchRemoteContextAsync(
|
||||
string contextRef,
|
||||
string cacheKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("Spdx3Context");
|
||||
client.Timeout = _options.HttpTimeout;
|
||||
|
||||
_logger.LogInformation("Fetching remote context {ContextRef}", contextRef);
|
||||
var content = await client.GetStringAsync(contextRef, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var context = CreateContext(contextRef, content);
|
||||
CacheContext(cacheKey, context);
|
||||
return context;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch remote context {ContextRef}", contextRef);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Spdx3Context CreateContext(string uri, string content)
|
||||
{
|
||||
return new Spdx3Context
|
||||
{
|
||||
Uri = uri,
|
||||
Document = JsonDocument.Parse(content),
|
||||
ResolvedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
private void CacheContext(string cacheKey, Spdx3Context context)
|
||||
{
|
||||
var cacheOptions = new MemoryCacheEntryOptions
|
||||
{
|
||||
Size = 1,
|
||||
SlidingExpiration = _options.CacheTtl
|
||||
};
|
||||
_cache.Set(cacheKey, context, cacheOptions);
|
||||
}
|
||||
|
||||
private static string GetContextFileName(string uri)
|
||||
{
|
||||
var lastSlash = uri.LastIndexOf('/');
|
||||
return lastSlash >= 0 ? uri[(lastSlash + 1)..] : uri;
|
||||
}
|
||||
|
||||
private static string GetEmbeddedContext(string name)
|
||||
{
|
||||
// In a real implementation, this would load from embedded resources
|
||||
// For now, return a minimal stub that allows parsing
|
||||
return name switch
|
||||
{
|
||||
"spdx-context.jsonld" => """
|
||||
{
|
||||
"@context": {
|
||||
"spdx": "https://spdx.org/rdf/3.0.1/terms/",
|
||||
"Core": "spdx:Core/",
|
||||
"Software": "spdx:Software/",
|
||||
"spdxId": "@id",
|
||||
"@type": "@type"
|
||||
}
|
||||
}
|
||||
""",
|
||||
_ => "{}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// IMemoryCache is typically managed by DI
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user