Files
git.stella-ops.org/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/Builders/EvidenceBundleBuilder.cs
2026-02-01 21:37:40 +02:00

123 lines
3.8 KiB
C#

using StellaOps.EvidenceLocker.Core.Builders;
using StellaOps.EvidenceLocker.Core.Domain;
using StellaOps.EvidenceLocker.Core.Repositories;
using System.Collections.Immutable;
using System.Text;
namespace StellaOps.EvidenceLocker.Infrastructure.Builders;
internal sealed class EvidenceBundleBuilder(
IEvidenceBundleRepository repository,
IMerkleTreeCalculator merkleTreeCalculator) : IEvidenceBundleBuilder
{
public async Task<EvidenceBundleBuildResult> BuildAsync(
EvidenceBundleBuildRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var manifest = CreateManifest(request);
var rootHash = merkleTreeCalculator.CalculateRootHash(
manifest.Entries.Select(entry => $"{entry.CanonicalPath}|{entry.Sha256}"));
await repository.SetBundleAssemblyAsync(
request.BundleId,
request.TenantId,
EvidenceBundleStatus.Sealed,
rootHash,
request.CreatedAt,
cancellationToken);
return new EvidenceBundleBuildResult(rootHash, manifest);
}
private static EvidenceBundleManifest CreateManifest(EvidenceBundleBuildRequest request)
{
var normalizedMetadata = request.Metadata?.OrderBy(pair => pair.Key, StringComparer.Ordinal)
.ToImmutableDictionary(pair => pair.Key, pair => pair.Value)
?? ImmutableDictionary<string, string>.Empty;
var entries = request.Materials?
.Select(material => CreateEntry(material))
.OrderBy(entry => entry.CanonicalPath, StringComparer.Ordinal)
.ToImmutableArray() ?? ImmutableArray<EvidenceManifestEntry>.Empty;
return new EvidenceBundleManifest(
request.BundleId,
request.TenantId,
request.Kind,
request.CreatedAt,
normalizedMetadata,
entries);
}
private static EvidenceManifestEntry CreateEntry(EvidenceBundleMaterial material)
{
var canonicalSection = NormalizeSection(material.Section);
var canonicalPath = $"{canonicalSection}/{NormalizePath(material.Path)}";
var attributes = material.Attributes?
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
.ToImmutableDictionary(pair => pair.Key, pair => pair.Value)
?? ImmutableDictionary<string, string>.Empty;
return new EvidenceManifestEntry(
canonicalSection,
canonicalPath,
material.Sha256.ToLowerInvariant(),
material.SizeBytes,
material.MediaType,
attributes);
}
private static string NormalizeSection(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "root";
}
return NormalizeSegment(value);
}
private static string NormalizePath(string value)
{
if (string.IsNullOrEmpty(value))
{
return "item";
}
var segments = value.Split(['/','\\'], StringSplitOptions.RemoveEmptyEntries);
var normalized = segments
.Where(segment => segment is not "." and not "..")
.Select(NormalizeSegment);
return string.Join('/', normalized);
}
private static string NormalizeSegment(string value)
{
Span<char> buffer = stackalloc char[value.Length];
var index = 0;
foreach (var ch in value.Trim())
{
if (char.IsLetterOrDigit(ch))
{
buffer[index++] = char.ToLowerInvariant(ch);
}
else if (ch is '-' or '_' || ch == '.')
{
buffer[index++] = ch;
}
else
{
buffer[index++] = '-';
}
}
return index > 0 ? new string(buffer[..index]) : "item";
}
}