using System; using System.IO; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Surface; internal static class SurfaceCasLayout { internal const string DefaultBucket = "scanner-artifacts"; internal const string DefaultRootPrefix = "scanner"; private const string Sha256 = "sha256"; public static string NormalizeDigest(string? digest) { if (string.IsNullOrWhiteSpace(digest)) { throw new BuildxPluginException("Surface artefact digest cannot be empty."); } var trimmed = digest.Trim(); return trimmed.Contains(':', StringComparison.Ordinal) ? trimmed : $"{Sha256}:{trimmed}"; } public static string ExtractDigestValue(string normalizedDigest) { var parts = normalizedDigest.Split(':', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); return parts.Length == 2 ? parts[1] : normalizedDigest; } public static string BuildObjectKey(string rootPrefix, SurfaceCasKind kind, string normalizedDigest) { var digestValue = ExtractDigestValue(normalizedDigest); var prefix = kind switch { SurfaceCasKind.LayerFragments => "surface/payloads/layer-fragments", SurfaceCasKind.EntryTraceGraph => "surface/payloads/entrytrace", SurfaceCasKind.EntryTraceNdjson => "surface/payloads/entrytrace", SurfaceCasKind.Manifest => "surface/manifests", _ => "surface/unknown" }; var extension = kind switch { SurfaceCasKind.LayerFragments => "layer-fragments.json", SurfaceCasKind.EntryTraceGraph => "entrytrace.graph.json", SurfaceCasKind.EntryTraceNdjson => "entrytrace.ndjson", SurfaceCasKind.Manifest => "surface.manifest.json", _ => "artifact.bin" }; var normalizedRoot = string.IsNullOrWhiteSpace(rootPrefix) ? string.Empty : rootPrefix.Trim().Trim('/'); var relative = $"{prefix}/{digestValue}/{extension}"; return string.IsNullOrWhiteSpace(normalizedRoot) ? relative : $"{normalizedRoot}/{relative}"; } public static string BuildCasUri(string bucket, string objectKey) { var normalizedBucket = string.IsNullOrWhiteSpace(bucket) ? DefaultBucket : bucket.Trim(); var normalizedKey = string.IsNullOrWhiteSpace(objectKey) ? string.Empty : objectKey.Trim().TrimStart('/'); return $"cas://{normalizedBucket}/{normalizedKey}"; } public static string ComputeDigest(ReadOnlySpan content) { Span hash = stackalloc byte[32]; SHA256.HashData(content, hash); return $"{Sha256}:{Convert.ToHexString(hash).ToLowerInvariant()}"; } public static async Task WriteBytesAsync(string rootDirectory, string objectKey, byte[] bytes, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(rootDirectory)) { throw new BuildxPluginException("Surface cache root must be provided."); } var normalizedRoot = Path.GetFullPath(rootDirectory); var relativePath = objectKey.Replace('/', Path.DirectorySeparatorChar); var fullPath = Path.Combine(normalizedRoot, relativePath); var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } await using var stream = new FileStream( fullPath, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 64 * 1024, options: FileOptions.Asynchronous | FileOptions.SequentialScan); await stream.WriteAsync(bytes.AsMemory(0, bytes.Length), cancellationToken).ConfigureAwait(false); await stream.FlushAsync(cancellationToken).ConfigureAwait(false); return fullPath; } } internal enum SurfaceCasKind { LayerFragments, EntryTraceGraph, EntryTraceNdjson, Manifest }