using System; using System.IO; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; namespace AVEVA.Historian.ReverseInstrumentation { public static class CaptureLogger { private static readonly object Gate = new object(); public static void LogBuffer(string phase, IntPtr data, ulong length) { try { int byteCount = checked((int)Math.Min(length, 1024UL * 1024UL)); byte[] bytes = new byte[byteCount]; if (data != IntPtr.Zero && byteCount > 0 && IsReadableMemoryRange(data, byteCount)) { Marshal.Copy(data, bytes, 0, byteCount); } string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE"); if (string.IsNullOrWhiteSpace(path)) { path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson"); } string directory = Path.GetDirectoryName(Path.GetFullPath(path)); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } string json = "{" + "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\"," + "\"Phase\":\"" + JsonEscape(phase) + "\"," + "\"Length\":" + length + "," + "\"CapturedLength\":" + byteCount + "," + "\"Sha256\":\"" + ComputeSha256(bytes) + "\"," + "\"Base64\":\"" + Convert.ToBase64String(bytes) + "\"" + "}"; lock (Gate) { File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8); } } catch { // Reverse-engineering instrumentation must not perturb the native query path. } } private static bool IsReadableMemoryRange(IntPtr address, int byteCount) { if (address == IntPtr.Zero || byteCount <= 0) { return false; } long current = address.ToInt64(); long end = checked(current + byteCount); while (current < end) { if (VirtualQuery(new IntPtr(current), out MEMORY_BASIC_INFORMATION info, (UIntPtr)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))) == UIntPtr.Zero) { return false; } if (info.State != MEM_COMMIT || (info.Protect & PAGE_GUARD) != 0 || (info.Protect & PAGE_NOACCESS) != 0) { return false; } long regionEnd = checked(info.BaseAddress.ToInt64() + (long)info.RegionSize.ToUInt64()); if (regionEnd <= current) { return false; } current = regionEnd; } return true; } private const uint MEM_COMMIT = 0x1000; private const uint PAGE_NOACCESS = 0x01; private const uint PAGE_GUARD = 0x100; [DllImport("kernel32.dll")] private static extern UIntPtr VirtualQuery(IntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, UIntPtr dwLength); [StructLayout(LayoutKind.Sequential)] private struct MEMORY_BASIC_INFORMATION { public IntPtr BaseAddress; public IntPtr AllocationBase; public uint AllocationProtect; public UIntPtr RegionSize; public uint State; public uint Protect; public uint Type; } public static void LogByteArraySegment(string phase, byte[] bytes, int offset, int count) { try { if (bytes == null || count <= 0 || offset < 0 || offset > bytes.Length) { WriteRecord(phase, 0UL, new byte[0]); return; } int captureCount = Math.Min(count, bytes.Length - offset); if (captureCount > 1024 * 1024) { captureCount = 1024 * 1024; } byte[] captured = new byte[captureCount]; Buffer.BlockCopy(bytes, offset, captured, 0, captureCount); WriteRecord(phase, (ulong)count, captured); } catch { // Reverse-engineering instrumentation must not perturb the native query path. } } public static void LogByteArray(string phase, byte[] bytes) { try { byte[] captured = bytes ?? new byte[0]; if (captured.Length > 1024 * 1024) { byte[] truncated = new byte[1024 * 1024]; Buffer.BlockCopy(captured, 0, truncated, 0, truncated.Length); captured = truncated; } WriteRecord(phase, bytes == null ? 0UL : (ulong)bytes.Length, captured); } catch { // Reverse-engineering instrumentation must not perturb the native query path. } } public static void LogByteArraySummary(string phase, byte[] bytes) { try { byte[] captured = bytes ?? new byte[0]; if (captured.Length > 1024 * 1024) { byte[] truncated = new byte[1024 * 1024]; Buffer.BlockCopy(captured, 0, truncated, 0, truncated.Length); captured = truncated; } WriteSummaryRecord(phase, bytes == null ? 0UL : (ulong)bytes.Length, captured); } catch { // Reverse-engineering instrumentation must not perturb the native query path. } } public static void LogString(string phase, string value) { try { string captured = value ?? string.Empty; byte[] bytes = Encoding.UTF8.GetBytes(captured); string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE"); if (string.IsNullOrWhiteSpace(path)) { path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson"); } string directory = Path.GetDirectoryName(Path.GetFullPath(path)); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } string json = "{" + "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\"," + "\"Phase\":\"" + JsonEscape(phase) + "\"," + "\"Length\":" + captured.Length + "," + "\"Sha256\":\"" + ComputeSha256(bytes) + "\"," + "\"Value\":\"" + JsonEscape(captured) + "\"" + "}"; lock (Gate) { File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8); } } catch { // Reverse-engineering instrumentation must not perturb the native query path. } } public static void LogStringSummary(string phase, string value) { try { string captured = value ?? string.Empty; byte[] bytes = Encoding.UTF8.GetBytes(captured); int hyphenCount = 0; int uppercaseCount = 0; int lowercaseCount = 0; int digitCount = 0; for (int index = 0; index < captured.Length; index++) { char current = captured[index]; if (current == '-') { hyphenCount++; } else if (current >= 'A' && current <= 'Z') { uppercaseCount++; } else if (current >= 'a' && current <= 'z') { lowercaseCount++; } else if (current >= '0' && current <= '9') { digitCount++; } } string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE"); if (string.IsNullOrWhiteSpace(path)) { path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson"); } string directory = Path.GetDirectoryName(Path.GetFullPath(path)); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } string json = "{" + "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\"," + "\"Phase\":\"" + JsonEscape(phase) + "\"," + "\"Length\":" + captured.Length + "," + "\"Sha256\":\"" + ComputeSha256(bytes) + "\"," + "\"HyphenCount\":" + hyphenCount + "," + "\"UppercaseCount\":" + uppercaseCount + "," + "\"LowercaseCount\":" + lowercaseCount + "," + "\"DigitCount\":" + digitCount + "," + "\"StartsWithBrace\":" + (captured.StartsWith("{", StringComparison.Ordinal) ? "true" : "false") + "," + "\"EndsWithBrace\":" + (captured.EndsWith("}", StringComparison.Ordinal) ? "true" : "false") + "," + "\"ContainsBackslash\":" + (captured.IndexOf('\\') >= 0 ? "true" : "false") + "}"; lock (Gate) { File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8); } } catch { // Reverse-engineering instrumentation must not perturb the native query path. } } public static void LogUInt32(string phase, uint value) { try { string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE"); if (string.IsNullOrWhiteSpace(path)) { path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson"); } string directory = Path.GetDirectoryName(Path.GetFullPath(path)); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } string json = "{" + "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\"," + "\"Phase\":\"" + JsonEscape(phase) + "\"," + "\"Value\":" + value + "}"; lock (Gate) { File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8); } } catch { // Reverse-engineering instrumentation must not perturb the native query path. } } public static void LogStdVector(string phase, IntPtr vector, ulong elementSize, ulong maxElements) { try { if (vector == IntPtr.Zero || elementSize == 0 || maxElements == 0) { WriteVectorRecord(phase, elementSize, 0, 0, 0, new byte[0]); return; } IntPtr begin = Marshal.ReadIntPtr(vector, 0); IntPtr end = Marshal.ReadIntPtr(vector, IntPtr.Size); if (begin == IntPtr.Zero || end == IntPtr.Zero) { WriteVectorRecord(phase, elementSize, 0, 0, 0, new byte[0]); return; } long byteLength = end.ToInt64() - begin.ToInt64(); if (byteLength <= 0 || byteLength % checked((long)elementSize) != 0) { WriteVectorRecord(phase, elementSize, 0, 0, 0, new byte[0]); return; } ulong count = checked((ulong)byteLength / elementSize); ulong capturedCount = Math.Min(count, maxElements); ulong capturedLength = Math.Min(checked(capturedCount * elementSize), 1024UL * 1024UL); byte[] captured = new byte[checked((int)capturedLength)]; if (captured.Length > 0) { Marshal.Copy(begin, captured, 0, captured.Length); } WriteVectorRecord(phase, elementSize, count, capturedCount, (ulong)byteLength, captured); } catch { // Reverse-engineering instrumentation must not perturb the native query path. } } private static void WriteSummaryRecord(string phase, ulong length, byte[] bytes) { string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE"); if (string.IsNullOrWhiteSpace(path)) { path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson"); } string directory = Path.GetDirectoryName(Path.GetFullPath(path)); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } string json = "{" + "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\"," + "\"Phase\":\"" + JsonEscape(phase) + "\"," + "\"Length\":" + length + "," + "\"CapturedLength\":" + bytes.Length + "," + "\"Sha256\":\"" + ComputeSha256(bytes) + "\"," + "\"PrefixHex\":\"" + ToPrefixHex(bytes, 32) + "\"," + "\"PrefixAscii\":\"" + JsonEscape(ToSafeAscii(bytes, 32)) + "\"" + "}"; lock (Gate) { File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8); } } private static void WriteRecord(string phase, ulong length, byte[] bytes) { string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE"); if (string.IsNullOrWhiteSpace(path)) { path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson"); } string directory = Path.GetDirectoryName(Path.GetFullPath(path)); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } string json = "{" + "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\"," + "\"Phase\":\"" + JsonEscape(phase) + "\"," + "\"Length\":" + length + "," + "\"CapturedLength\":" + bytes.Length + "," + "\"Sha256\":\"" + ComputeSha256(bytes) + "\"," + "\"Base64\":\"" + Convert.ToBase64String(bytes) + "\"" + "}"; lock (Gate) { File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8); } } private static void WriteVectorRecord( string phase, ulong elementSize, ulong count, ulong capturedCount, ulong length, byte[] bytes) { string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE"); if (string.IsNullOrWhiteSpace(path)) { path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson"); } string directory = Path.GetDirectoryName(Path.GetFullPath(path)); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } string json = "{" + "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\"," + "\"Phase\":\"" + JsonEscape(phase) + "\"," + "\"ElementSize\":" + elementSize + "," + "\"Count\":" + count + "," + "\"CapturedCount\":" + capturedCount + "," + "\"Length\":" + length + "," + "\"CapturedLength\":" + bytes.Length + "," + "\"Sha256\":\"" + ComputeSha256(bytes) + "\"," + "\"Base64\":\"" + Convert.ToBase64String(bytes) + "\"" + "}"; lock (Gate) { File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8); } } private static string ComputeSha256(byte[] bytes) { using (SHA256 sha256 = SHA256.Create()) { byte[] hash = sha256.ComputeHash(bytes); StringBuilder builder = new StringBuilder(hash.Length * 2); foreach (byte value in hash) { builder.Append(value.ToString("x2")); } return builder.ToString(); } } private static string ToPrefixHex(byte[] bytes, int maxBytes) { int count = Math.Min(bytes.Length, maxBytes); StringBuilder builder = new StringBuilder(count * 2); for (int index = 0; index < count; index++) { builder.Append(bytes[index].ToString("x2")); } return builder.ToString(); } private static string ToSafeAscii(byte[] bytes, int maxBytes) { int count = Math.Min(bytes.Length, maxBytes); StringBuilder builder = new StringBuilder(count); for (int index = 0; index < count; index++) { byte value = bytes[index]; builder.Append(value >= 32 && value <= 126 ? (char)value : '.'); } return builder.ToString(); } private static string JsonEscape(string value) { return value .Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\r", "\\r") .Replace("\n", "\\n"); } } }