c95824a65d
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>
284 lines
9.6 KiB
C#
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)]);
|
|
}
|
|
}
|