Files
dohertj2 c95824a65d 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>
2026-05-04 06:31:48 -04:00

284 lines
9.6 KiB
C#

using System.Text;
using AVEVA.Historian.Client.Wcf;
namespace AVEVA.Historian.Client.Tests;
public sealed class WcfOpen2ProtocolTests
{
[Fact]
public void LegacyVersion1SerializerMatchesDecompiledSaveOpenConnectionParamsLayout()
{
byte[] actual = HistorianOpen2Protocol.SerializeLegacyVersion1(new HistorianOpen2Request(
HostName: "H",
ProcessName: "P",
ProcessId: 0x01020304,
UserName: "U",
Password: Encoding.Unicode.GetBytes("pw"),
ClientType: 4,
ClientVersion: 11,
ConnectionMode: 2,
MetadataNamespace: HistorianMetadataNamespace.Empty));
byte[] expected =
[
0x01, 0x00,
0x01, 0x00, 0x00, 0x00, 0x48, 0x00,
0x01, 0x00, 0x00, 0x00, 0x50, 0x00,
0x04, 0x03, 0x02, 0x01,
0x01, 0x00, 0x00, 0x00, 0x55, 0x00,
0x04, 0x00, 0x00, 0x00, 0x70, 0x00, 0x77, 0x00,
0x04,
0x0B, 0x00,
0x02, 0x00, 0x00, 0x00,
0x01,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
];
Assert.Equal(expected, actual);
}
[Fact]
public void LegacyVersion1SerializerUsesUtf16CodeUnitStringLengths()
{
byte[] actual = HistorianOpen2Protocol.SerializeLegacyVersion1(new HistorianOpen2Request(
HostName: "A\ud83d\ude00",
ProcessName: string.Empty,
ProcessId: 0,
UserName: string.Empty,
Password: [],
ClientType: 4,
ClientVersion: 0,
ConnectionMode: 2,
MetadataNamespace: HistorianMetadataNamespace.Empty));
Assert.Equal([0x03, 0x00, 0x00, 0x00], actual[2..6]);
Assert.Equal(Encoding.Unicode.GetBytes("A\ud83d\ude00"), actual[6..12]);
}
[Fact]
public void NativeErrorParserReadsObservedFiveByteBuffers()
{
HistorianNativeError? error = HistorianOpen2Protocol.TryReadNativeError([0x04, 0xAB, 0x00, 0x00, 0x00]);
Assert.NotNull(error);
Assert.Equal(4, error.Type);
Assert.Equal<uint>(171, error.Code);
Assert.Equal("AuthenticationFailed", error.Name);
}
[Fact]
public void NativeErrorParserRejectsShortBuffers()
{
Assert.Null(HistorianOpen2Protocol.TryReadNativeError([0x04, 0xAB, 0x00, 0x00]));
}
[Fact]
public void LegacyOpen2OutputParserReadsObservedWcfLayout()
{
byte[] buffer =
[
0x78, 0x56, 0x34, 0x12,
0x33, 0x22, 0x11, 0x00,
0x55, 0x44,
0x77, 0x66,
0x88, 0x99, 0xAA, 0xBB,
0xCC, 0xDD, 0xEE, 0xFF,
0x08, 0x07, 0x06, 0x05,
0x04, 0x03, 0x02, 0x01,
0x44, 0x33, 0x22, 0x11
];
HistorianLegacyOpen2Output? output = HistorianOpen2Protocol.TryReadLegacyOpen2Output(buffer);
Assert.NotNull(output);
Assert.Equal<uint>(0x12345678, output.Handle);
Assert.Equal(new Guid("00112233-4455-6677-8899-aabbccddeeff"), output.StorageSessionId);
Assert.Equal(0x0102030405060708, output.ConnectTimeFileTimeUtc);
Assert.Equal<uint>(0x11223344, output.ServerStatus);
}
[Fact]
public void LegacyOpen2OutputParserRejectsNonLegacyLength()
{
Assert.Null(HistorianOpen2Protocol.TryReadLegacyOpen2Output([0x00]));
}
[Fact]
public void NativeOpen3OutputParserReadsObservedDeserializerLayout()
{
byte[] buffer =
[
0x03,
0x78, 0x56, 0x34, 0x12,
0x33, 0x22, 0x11, 0x00,
0x55, 0x44,
0x77, 0x66,
0x88, 0x99, 0xAA, 0xBB,
0xCC, 0xDD, 0xEE, 0xFF,
0x08, 0x07, 0x06, 0x05,
0x04, 0x03, 0x02, 0x01,
0x18, 0x17, 0x16, 0x15,
0x14, 0x13, 0x12, 0x11,
0x44, 0x33, 0x22, 0x11,
0x00
];
HistorianNativeOpen3Output? output = HistorianOpen2Protocol.TryReadNativeOpen3Output(buffer);
Assert.NotNull(output);
Assert.Equal(3, output.ProtocolVersion);
Assert.Equal<uint>(0x12345678, output.Handle);
Assert.Equal(new Guid("00112233-4455-6677-8899-aabbccddeeff"), output.StorageSessionId);
Assert.Equal(0x0102030405060708, output.ConnectTimeFileTimeUtc);
Assert.Equal(0x1112131415161718, output.ServerTimeFileTimeUtc);
Assert.Equal([0x44, 0x33, 0x22, 0x11, 0x00], output.TrailingBytes);
}
[Fact]
public void NativeOpen3OutputParserRejectsUnsupportedVersion()
{
Assert.Null(HistorianOpen2Protocol.TryReadNativeOpen3Output([0x01, 0x00, 0x00, 0x00]));
}
[Fact]
public void NativeVersion3SerializerMatchesDecompiledFieldOrder()
{
byte[] actual = HistorianOpen2Protocol.SerializeNativeVersion3(
new HistorianOpen2Request(
HostName: "H",
ProcessName: "P",
ProcessId: 0x01020304,
UserName: string.Empty,
Password: [0xAA, 0xBB],
ClientType: 4,
ClientVersion: 11,
ConnectionMode: 1026,
MetadataNamespace: HistorianMetadataNamespace.Empty),
new HistorianClientCommonInfo(
FormatVersion: 3,
ServerNodeName: "S",
ClientNodeName: "C",
ProcessId: 0x11223344,
HcalVersion: 17,
ProcessName: "Proc",
Proxy: string.Empty,
DataSourceId: string.Empty,
ShardId: new Guid("00112233-4455-6677-8899-aabbccddeeff"),
ClientVersion: 0x55667788,
ClientTimestamp: 0x0102030405060708,
ClientDllVersion: string.Empty));
byte[] expectedPrefix =
[
0x03,
0x01, 0x00, 0x00, 0x00, 0x48, 0x00,
0x02, 0x00, 0xAA, 0xBB,
0x04,
0x02, 0x04, 0x00, 0x00
];
Assert.Equal(expectedPrefix, actual[..expectedPrefix.Length]);
Assert.Contains<byte>(0x03, actual);
byte[] expectedSuffix = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
Assert.Equal(expectedSuffix, actual[^expectedSuffix.Length..]);
}
[Fact]
public void NativeOpenConnection3Version6SerializerAddsObservedPrefixBeforeContent()
{
HistorianOpen2Request request = new(
HostName: "H",
ProcessName: "P",
ProcessId: 0x01020304,
UserName: string.Empty,
Password: [0xAA, 0xBB],
ClientType: 4,
ClientVersion: 11,
ConnectionMode: 1026,
MetadataNamespace: HistorianMetadataNamespace.Empty);
HistorianClientCommonInfo commonInfo = new(
FormatVersion: 3,
ServerNodeName: "S",
ClientNodeName: "C",
ProcessId: 0x11223344,
HcalVersion: 17,
ProcessName: "Proc",
Proxy: string.Empty,
DataSourceId: string.Empty,
ShardId: new Guid("00112233-4455-6677-8899-aabbccddeeff"),
ClientVersion: 0x55667788,
ClientTimestamp: 0x0102030405060708,
ClientDllVersion: string.Empty);
byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6(
request,
commonInfo,
new Guid("00112233-4455-6677-8899-aabbccddeeff"));
byte[] expectedPrefix =
[
0x06,
0x33, 0x22, 0x11, 0x00,
0x55, 0x44,
0x77, 0x66,
0x88, 0x99, 0xAA, 0xBB,
0xCC, 0xDD, 0xEE, 0xFF,
0x00
];
Assert.Equal(expectedPrefix, actual[..expectedPrefix.Length]);
byte[] expectedContentPrefix =
[
0x01, 0x00, 0x00, 0x00, 0x48, 0x00,
0x02, 0x00, 0xAA, 0xBB,
0x04,
0x02, 0x04, 0x00, 0x00,
0x01,
0x01, 0x00, 0x00,
0x01, 0x00, 0x00,
0x01, 0x00, 0x00
];
Assert.Equal(expectedContentPrefix, actual[expectedPrefix.Length..(expectedPrefix.Length + expectedContentPrefix.Length)]);
}
[Fact]
public void NativeOpenConnection3Version6SerializerCanUseSeparateCredentialBlock()
{
HistorianOpen2Request request = new(
HostName: "H",
ProcessName: "P",
ProcessId: 0x01020304,
UserName: string.Empty,
Password: [0xAA, 0xBB],
ClientType: 4,
ClientVersion: 11,
ConnectionMode: 1026,
MetadataNamespace: HistorianMetadataNamespace.Empty);
HistorianClientCommonInfo commonInfo = new(
FormatVersion: 2,
ServerNodeName: string.Empty,
ClientNodeName: string.Empty,
ProcessId: 0,
HcalVersion: 17,
ProcessName: string.Empty,
Proxy: string.Empty,
DataSourceId: string.Empty,
ShardId: Guid.Empty,
ClientVersion: 0,
ClientTimestamp: 0,
ClientDllVersion: string.Empty);
byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6(
request,
commonInfo,
Guid.Empty,
[0x00, 0x00, 0x00, 0x00]);
int hostLengthOffset = 18;
int credentialLengthOffset = hostLengthOffset + 4 + Encoding.Unicode.GetByteCount("H");
Assert.Equal([0x04, 0x00], actual[credentialLengthOffset..(credentialLengthOffset + 2)]);
Assert.Equal([0x00, 0x00, 0x00, 0x00], actual[(credentialLengthOffset + 2)..(credentialLengthOffset + 6)]);
}
}