using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace AVEVA.Historian.ReverseEngineering.Capture;
/// One captured operation: the (optional) request buffer and the response buffer, raw.
public sealed record ProtocolCapture(string Op, byte[]? Request, byte[]? Response, string? Notes = null);
///
/// CW-1 fixture writer: takes a live , redacts it with
/// , and writes a committable JSON fixture under
/// fixtures/protocol/<op>/. The fixture records sanitized hex, lengths, SHA-256 of the
/// sanitized bytes, and the scrub report — never the original identity-bearing bytes.
///
/// Timestamps are passed in (never generated here) so the writer stays deterministic and testable.
///
public static class ProtocolFixtureWriter
{
public static string BuildFixtureJson(
ProtocolCapture capture,
IReadOnlyList secrets,
string capturedUtcIso)
{
ArgumentNullException.ThrowIfNull(capture);
BufferSection? request = BuildSection(capture.Request, secrets);
BufferSection? response = BuildSection(capture.Response, secrets);
var document = new
{
op = capture.Op,
capturedUtc = capturedUtcIso,
notes = capture.Notes,
request,
response,
};
return JsonSerializer.Serialize(document, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
}
/// Serializes the fixture and writes it to /<op>/<name>.json. Returns the path.
public static string Write(
string fixtureRoot,
string name,
ProtocolCapture capture,
IReadOnlyList secrets,
string capturedUtcIso)
{
ArgumentException.ThrowIfNullOrWhiteSpace(fixtureRoot);
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(capture);
string json = BuildFixtureJson(capture, secrets, capturedUtcIso);
string directory = Path.Combine(fixtureRoot, capture.Op);
Directory.CreateDirectory(directory);
string path = Path.Combine(directory, name + ".json");
File.WriteAllText(path, json, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
return path;
}
private static BufferSection? BuildSection(byte[]? raw, IReadOnlyList secrets)
{
if (raw is null)
{
return null;
}
SanitizeResult result = ProtocolCaptureSanitizer.Sanitize(raw, secrets);
ProtocolCaptureSanitizer.AssertNoSecretsRemain(result.Sanitized, secrets);
return new BufferSection(
Length: raw.Length,
Sha256: Convert.ToHexString(SHA256.HashData(result.Sanitized)).ToLowerInvariant(),
Hex: Convert.ToHexString(result.Sanitized).ToLowerInvariant(),
Redactions: result.Report
.Where(r => r.Total > 0)
.Select(r => new RedactionEntry(r.Name, r.AsciiMatches, r.Utf16Matches))
.ToArray());
}
private sealed record BufferSection(int Length, string Sha256, string Hex, IReadOnlyList Redactions);
private sealed record RedactionEntry(string Secret, int AsciiMatches, int Utf16Matches);
}