From ba11407be48fb6d96b6c3fcc57517481cc61b292 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 6 Jan 2026 16:55:18 -0500 Subject: [PATCH] feat(DbExporter): implement verify and verify-full Add Verifier class that reads exported .pb.zstd files and: - Deserializes protobuf data through zstd decompression - Extracts schema information (column names and types) - Counts rows for verification - Optionally computes SHA256 hash and compares against sidecar file --- Tools/DbExporter/Verifier.cs | 79 ++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Tools/DbExporter/Verifier.cs diff --git a/Tools/DbExporter/Verifier.cs b/Tools/DbExporter/Verifier.cs new file mode 100644 index 0000000..0179756 --- /dev/null +++ b/Tools/DbExporter/Verifier.cs @@ -0,0 +1,79 @@ +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(); + } +}