using System.Data; using System.Security.Cryptography; using System.Text; using ProtoBuf.Data; using ZstdSharp; namespace DbExporter; public sealed class Verifier { public record VerifyResult(int RowCount, List Schema, string? ComputedHash, string? ExpectedHash, bool? HashMatch); public record ColumnInfo(string Name, Type Type); public VerifyResult Verify(string filePath, bool computeHash = false) { using var inputFile = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 256 * 1024); using var decompressStream = new DecompressionStream(inputFile); Stream readStream = decompressStream; SHA256? sha256 = null; CryptoStream? hashStream = null; if (computeHash) { sha256 = SHA256.Create(); hashStream = new CryptoStream(decompressStream, sha256, CryptoStreamMode.Read); readStream = hashStream; } using var reader = DataSerializer.Deserialize(readStream); // Extract schema var schema = new List(); for (int i = 0; i < reader.FieldCount; i++) { schema.Add(new ColumnInfo(reader.GetName(i), reader.GetFieldType(i))); } // Count rows int rowCount = 0; while (reader.Read()) { rowCount++; } string? computedHashStr = null; string? expectedHash = null; bool? hashMatch = null; if (computeHash && sha256 != null) { hashStream?.Dispose(); computedHashStr = Convert.ToHexString(sha256.Hash!).ToLowerInvariant(); // Read expected hash from sidecar var hashFilePath = filePath + ".sha256"; if (File.Exists(hashFilePath)) { expectedHash = File.ReadAllText(hashFilePath).Trim().ToLowerInvariant(); hashMatch = computedHashStr == expectedHash; } sha256.Dispose(); } return new VerifyResult(rowCount, schema, computedHashStr, expectedHash, hashMatch); } public string FormatSchema(List schema) { var sb = new StringBuilder(); foreach (var col in schema) { if (sb.Length > 0) sb.Append(", "); sb.Append($"{col.Name} ({col.Type.Name})"); } return sb.ToString(); } }