using StellaOps.Scanner.Cache.Abstractions; using System; using System.IO; using System.IO.Compression; using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Scanner.Reachability; /// /// Packages a reachability union graph into a deterministic zip, stores it in CAS, and returns the CAS reference. /// public sealed class ReachabilityUnionPublisher { private readonly ReachabilityUnionWriter writer; public ReachabilityUnionPublisher(ReachabilityUnionWriter writer) { this.writer = writer ?? throw new ArgumentNullException(nameof(writer)); } public async Task PublishAsync( ReachabilityUnionGraph graph, IFileContentAddressableStore cas, string workRoot, string analysisId, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(graph); ArgumentNullException.ThrowIfNull(cas); ArgumentException.ThrowIfNullOrWhiteSpace(workRoot); ArgumentException.ThrowIfNullOrWhiteSpace(analysisId); var result = await writer.WriteAsync(graph, workRoot, analysisId, cancellationToken).ConfigureAwait(false); var folder = Path.GetDirectoryName(result.MetaPath)!; var zipPath = Path.Combine(folder, "reachability.zip"); CreateZip(folder, zipPath); var sha = ComputeSha256(zipPath); await using var zipStream = File.OpenRead(zipPath); var casEntry = await cas.PutAsync(new FileCasPutRequest(sha, zipStream, leaveOpen: false), cancellationToken).ConfigureAwait(false); return new ReachabilityUnionPublishResult( Sha256: sha, RelativePath: casEntry.RelativePath, Records: result.Nodes.RecordCount + result.Edges.RecordCount + (result.Facts?.RecordCount ?? 0)); } private static void CreateZip(string sourceDir, string destinationZip) { if (File.Exists(destinationZip)) { File.Delete(destinationZip); } var files = Directory.EnumerateFiles(sourceDir, "*", SearchOption.TopDirectoryOnly) .OrderBy(f => f, StringComparer.Ordinal) .ToList(); using var zip = ZipFile.Open(destinationZip, ZipArchiveMode.Create); foreach (var file in files) { var entryName = Path.GetFileName(file); zip.CreateEntryFromFile(file, entryName, CompressionLevel.Optimal); } } private static string ComputeSha256(string path) { using var sha = SHA256.Create(); using var stream = File.OpenRead(path); return Convert.ToHexString(sha.ComputeHash(stream)).ToLowerInvariant(); } } public sealed record ReachabilityUnionPublishResult( string Sha256, string RelativePath, int Records);