Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit

Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:

- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass

Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.

Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
dohertj2
2026-05-04 06:31:48 -04:00
commit c95824a65d
230 changed files with 38666 additions and 0 deletions
@@ -0,0 +1,275 @@
using System.Buffers.Binary;
using System.Text;
namespace AVEVA.Historian.Client.Wcf;
internal static class HistorianOpen2Protocol
{
public static byte[] SerializeLegacyVersion1(HistorianOpen2Request request)
{
using MemoryStream stream = new();
using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true);
writer.Write((ushort)1);
WriteHistorianString(writer, request.HostName);
WriteHistorianString(writer, request.ProcessName);
writer.Write(request.ProcessId);
WriteHistorianString(writer, request.UserName);
writer.Write((uint)request.Password.Length);
writer.Write(request.Password);
writer.Write(request.ClientType);
writer.Write(request.ClientVersion);
writer.Write(request.ConnectionMode);
WriteMetadataNamespace(writer, request.MetadataNamespace);
return stream.ToArray();
}
public static byte[] SerializeNativeVersion3(HistorianOpen2Request request, HistorianClientCommonInfo commonInfo)
{
using MemoryStream stream = new();
using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true);
writer.Write((byte)3);
WriteNativeOpenConnectionContent(writer, request, commonInfo);
return stream.ToArray();
}
public static byte[] SerializeNativeOpenConnection3Version6(
HistorianOpen2Request request,
HistorianClientCommonInfo commonInfo,
Guid clientKey,
byte[]? credentialBlock = null)
{
using MemoryStream stream = new();
using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true);
writer.Write((byte)6);
writer.Write(clientKey.ToByteArray());
writer.Write((byte)0);
WriteNativeOpenConnectionContent(writer, request, commonInfo, credentialBlock, useCompactMetadataNamespace: true);
return stream.ToArray();
}
private static void WriteNativeOpenConnectionContent(
BinaryWriter writer,
HistorianOpen2Request request,
HistorianClientCommonInfo commonInfo,
byte[]? credentialBlock = null,
bool useCompactMetadataNamespace = false)
{
byte[] secretBytes = credentialBlock ?? request.Password;
WriteHistorianString(writer, request.HostName);
checked
{
writer.Write((ushort)secretBytes.Length);
}
writer.Write(secretBytes);
writer.Write(request.ClientType);
writer.Write(request.ConnectionMode);
if (useCompactMetadataNamespace)
{
WriteCompactMetadataNamespace(writer, request.MetadataNamespace);
}
else
{
WriteMetadataNamespace(writer, request.MetadataNamespace);
}
WriteHistorianString(writer, string.Empty);
WriteHistorianString(writer, string.Empty);
WriteClientCommonInfo(writer, commonInfo);
}
public static HistorianNativeError? TryReadNativeError(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < 5)
{
return null;
}
byte type = buffer[0];
uint code = BinaryPrimitives.ReadUInt32LittleEndian(buffer[1..5]);
return new HistorianNativeError(type, code, GetKnownErrorName(code));
}
public static HistorianLegacyOpen2Output? TryReadLegacyOpen2Output(ReadOnlySpan<byte> buffer)
{
if (buffer.Length != 32)
{
return null;
}
uint handle = BinaryPrimitives.ReadUInt32LittleEndian(buffer[..4]);
Guid storageSessionId = new(buffer.Slice(4, 16));
long connectTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(20, 8));
uint serverStatus = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(28, 4));
return new HistorianLegacyOpen2Output(handle, storageSessionId, connectTime, serverStatus);
}
public static HistorianNativeOpen3Output? TryReadNativeOpen3Output(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < 29)
{
return null;
}
byte protocolVersion = buffer[0];
if (protocolVersion is not (2 or 3))
{
return null;
}
int minimumLength = protocolVersion >= 3 ? 37 : 29;
if (buffer.Length < minimumLength)
{
return null;
}
uint handle = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(1, 4));
Guid storageSessionId = new(buffer.Slice(5, 16));
long connectTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(21, 8));
long? serverTime = null;
if (protocolVersion >= 3)
{
serverTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(29, 8));
}
byte[] trailingBytes = buffer[minimumLength..].ToArray();
return new HistorianNativeOpen3Output(
protocolVersion,
handle,
storageSessionId,
connectTime,
serverTime,
trailingBytes);
}
public static byte[] EncodeWidePassword(string password)
{
return string.IsNullOrEmpty(password) ? [] : Encoding.Unicode.GetBytes(password);
}
private static void WriteMetadataNamespace(BinaryWriter writer, HistorianMetadataNamespace metadataNamespace)
{
writer.Write(metadataNamespace.HasValue ? (byte)1 : (byte)0);
WriteHistorianString(writer, metadataNamespace.Namespace);
WriteHistorianString(writer, metadataNamespace.TagPrefix);
WriteHistorianString(writer, metadataNamespace.PropertyPrefix);
}
private static void WriteCompactMetadataNamespace(BinaryWriter writer, HistorianMetadataNamespace metadataNamespace)
{
if (!metadataNamespace.HasValue
|| metadataNamespace.Namespace.Length != 0
|| metadataNamespace.TagPrefix.Length != 0
|| metadataNamespace.PropertyPrefix.Length != 0)
{
throw new ProtocolEvidenceMissingException("OpenConnection3 non-empty metadata namespace");
}
writer.Write((byte)1);
WriteCompactEmptyString(writer);
WriteCompactEmptyString(writer);
WriteCompactEmptyString(writer);
}
private static void WriteCompactEmptyString(BinaryWriter writer)
{
writer.Write((ushort)1);
writer.Write((byte)0);
}
private static void WriteHistorianString(BinaryWriter writer, string value)
{
writer.Write((uint)value.Length);
if (value.Length > 0)
{
writer.Write(Encoding.Unicode.GetBytes(value));
}
}
private static void WriteClientCommonInfo(BinaryWriter writer, HistorianClientCommonInfo commonInfo)
{
writer.Write(commonInfo.FormatVersion);
WriteHistorianString(writer, commonInfo.ServerNodeName);
WriteHistorianString(writer, commonInfo.ClientNodeName);
writer.Write(commonInfo.ProcessId);
writer.Write(commonInfo.HcalVersion);
WriteHistorianString(writer, commonInfo.ProcessName);
WriteHistorianString(writer, commonInfo.Proxy);
WriteHistorianString(writer, commonInfo.DataSourceId);
writer.Write(commonInfo.ShardId.ToByteArray());
writer.Write(commonInfo.ClientVersion);
if (commonInfo.FormatVersion >= 3)
{
writer.Write(commonInfo.ClientTimestamp);
}
if (commonInfo.FormatVersion >= 4)
{
WriteHistorianString(writer, commonInfo.ClientDllVersion);
}
}
private static string? GetKnownErrorName(uint code)
{
return code switch
{
1 => "Failure",
73 => "InvalidPacketVersion",
171 => "AuthenticationFailed",
_ => null
};
}
}
internal sealed record HistorianOpen2Request(
string HostName,
string ProcessName,
uint ProcessId,
string UserName,
byte[] Password,
byte ClientType,
ushort ClientVersion,
uint ConnectionMode,
HistorianMetadataNamespace MetadataNamespace);
internal sealed record HistorianMetadataNamespace(
bool HasValue,
string Namespace,
string TagPrefix,
string PropertyPrefix)
{
public static HistorianMetadataNamespace Empty { get; } = new(true, string.Empty, string.Empty, string.Empty);
}
internal sealed record HistorianNativeError(byte Type, uint Code, string? Name);
internal sealed record HistorianLegacyOpen2Output(
uint Handle,
Guid StorageSessionId,
long ConnectTimeFileTimeUtc,
uint ServerStatus);
internal sealed record HistorianNativeOpen3Output(
byte ProtocolVersion,
uint Handle,
Guid StorageSessionId,
long ConnectTimeFileTimeUtc,
long? ServerTimeFileTimeUtc,
byte[] TrailingBytes);
internal sealed record HistorianClientCommonInfo(
byte FormatVersion,
string ServerNodeName,
string ClientNodeName,
uint ProcessId,
ushort HcalVersion,
string ProcessName,
string Proxy,
string DataSourceId,
Guid ShardId,
uint ClientVersion,
ulong ClientTimestamp,
string ClientDllVersion);