using System.Buffers.Binary;
using System.Text;
using AVEVA.Historian.Client.Models;
namespace AVEVA.Historian.Client.Wcf;
///
/// Parser for the version-9 event-row buffer the Historian server returns from
/// /Retr/GetNextEventQueryResultBuffer.pResultBuff. Wire shape decoded from a captured
/// native event read (instrument-wcf-readmessage record 24, two rows for Alarm.Set + Alarm.Clear):
///
///
/// UInt16 version = 9
/// UInt32 rowCount
/// rowCount × Row {
/// UInt32 rowMarker = 0x1E
/// UInt16 rowFormat = 7
/// Int64 eventTimeUtcFiletime
/// UInt16 × 8 // purpose unclear (slot offsets?)
/// compact ASCII string // event type (Alarm.Set, Alarm.Clear, ...)
/// UInt16 propertyCount
/// propertyCount × Property {
/// compact ASCII string // property name
/// Value {
/// UInt8 typeMarker
/// UInt8 length // bytes of value following status
/// UInt8 status // observed 0x00 in successful captures
/// length × byte // encoding determined by typeMarker:
/// 0x02 → Boolean (1 byte: 0/1)
/// 0x10 → GUID (16 bytes)
/// 0x18 → FILETIME UTC (Int64)
/// 0x31 → Int32 little-endian
/// 0x43 → UTF-16 string: UInt16 charCount + charCount × UInt16 chars
/// }
/// }
/// }
///
///
/// Compact ASCII string: 0x09 LEN 0x00 LEN×ASCII bytes (same encoding as
/// CTagMetadata strings).
///
internal static class HistorianEventRowProtocol
{
public const ushort EventRowProtocolVersion = 9;
public const uint RowMarker = 0x0000001Eu;
public const ushort RowFormatV9 = 7;
private const int HeaderSize = 6;
private const int RowFixedHeaderSize = 4 + 2 + 8 + 16;
private const byte ValueTypeBool = 0x02;
private const byte ValueTypeGuid = 0x10;
private const byte ValueTypeFiletime = 0x18;
private const byte ValueTypeInt32 = 0x31;
private const byte ValueTypeUtf16String = 0x43;
public static IReadOnlyList Parse(ReadOnlySpan buffer)
{
if (buffer.Length < HeaderSize)
{
return [];
}
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(buffer[..2]);
if (version != EventRowProtocolVersion)
{
return [];
}
uint rowCount = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(2, 4));
if (rowCount == 0)
{
return [];
}
List events = new(checked((int)rowCount));
int cursor = HeaderSize;
for (uint rowIndex = 0; rowIndex < rowCount; rowIndex++)
{
if (!TryReadRow(buffer, ref cursor, out HistorianEvent? row))
{
break;
}
events.Add(row);
}
return events;
}
private static bool TryReadRow(ReadOnlySpan buffer, ref int cursor, out HistorianEvent row)
{
row = null!;
if (cursor + RowFixedHeaderSize > buffer.Length)
{
return false;
}
uint marker = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(cursor, 4));
if (marker != RowMarker)
{
return false;
}
ushort format = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(cursor + 4, 2));
if (format != RowFormatV9)
{
return false;
}
long filetime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(cursor + 6, 8));
DateTime eventTimeUtc = DateTime.FromFileTimeUtc(filetime);
int afterFixedHeader = cursor + RowFixedHeaderSize;
if (!TryReadCompactAsciiString(buffer, afterFixedHeader, out string eventType, out int afterType))
{
return false;
}
if (afterType + 2 > buffer.Length)
{
return false;
}
ushort propertyCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(afterType, 2));
int propertyCursor = afterType + 2;
Dictionary properties = new(propertyCount, StringComparer.OrdinalIgnoreCase);
for (int p = 0; p < propertyCount; p++)
{
if (!TryReadCompactAsciiString(buffer, propertyCursor, out string name, out int afterName))
{
return false;
}
if (!TryReadValue(buffer, afterName, out object? value, out int afterValue))
{
return false;
}
properties[name] = value;
propertyCursor = afterValue;
}
row = BuildEvent(eventTimeUtc, eventType, properties);
cursor = propertyCursor;
return true;
}
private static HistorianEvent BuildEvent(DateTime eventTimeUtc, string eventType, Dictionary properties)
{
Guid id = TryGetGuid(properties, "alarm_id") ?? Guid.Empty;
DateTime receivedTime = TryGetFiletime(properties, "receivedtime") ?? eventTimeUtc;
string sourceName = TryGetString(properties, "source_processvariable") ?? TryGetString(properties, "source_object") ?? string.Empty;
string ns = TryGetString(properties, "namespace") ?? TryGetString(properties, "provider_system") ?? string.Empty;
ushort revisionVersion = TryGetInt32(properties, "revisionversion") is int rv && rv is >= 0 and <= ushort.MaxValue
? (ushort)rv
: (ushort)0;
return new HistorianEvent(
Id: id,
EventTimeUtc: eventTimeUtc,
ReceivedTimeUtc: receivedTime,
Type: eventType,
SourceName: sourceName,
Namespace: ns,
RevisionVersion: revisionVersion,
Properties: properties);
}
private static Guid? TryGetGuid(Dictionary properties, string key) =>
properties.TryGetValue(key, out object? value) && value is Guid g ? g : null;
private static DateTime? TryGetFiletime(Dictionary properties, string key) =>
properties.TryGetValue(key, out object? value) && value is DateTime dt ? dt : null;
private static string? TryGetString(Dictionary properties, string key) =>
properties.TryGetValue(key, out object? value) && value is string s ? s : null;
private static int? TryGetInt32(Dictionary properties, string key) =>
properties.TryGetValue(key, out object? value) && value is int i ? i : null;
///
/// Compact ASCII string encoding: 0x09 LEN 0x00 LEN×ASCII bytes.
///
private static bool TryReadCompactAsciiString(ReadOnlySpan buffer, int offset, out string value, out int afterOffset)
{
value = string.Empty;
afterOffset = offset;
if (offset + 3 > buffer.Length || buffer[offset] != 0x09)
{
return false;
}
byte length = buffer[offset + 1];
int payloadStart = offset + 3;
if (payloadStart + length > buffer.Length)
{
return false;
}
value = Encoding.ASCII.GetString(buffer.Slice(payloadStart, length));
afterOffset = payloadStart + length;
return true;
}
///
/// Value encoding: typeMarker(1) + length(1) + status(1) + length×value bytes.
/// Decodes the value by typeMarker; unknown markers preserve the raw bytes as a
/// in the property bag.
///
private static bool TryReadValue(ReadOnlySpan buffer, int offset, out object? value, out int afterOffset)
{
value = null;
afterOffset = offset;
if (offset + 3 > buffer.Length)
{
return false;
}
byte typeMarker = buffer[offset];
byte length = buffer[offset + 1];
// buffer[offset + 2] is the status byte (observed 0x00 in successful captures).
int valueStart = offset + 3;
if (valueStart + length > buffer.Length)
{
return false;
}
ReadOnlySpan valueBytes = buffer.Slice(valueStart, length);
value = typeMarker switch
{
ValueTypeBool when length >= 1 => valueBytes[0] != 0,
ValueTypeGuid when length == 16 => new Guid(valueBytes),
ValueTypeFiletime when length == 8 => DateTime.FromFileTimeUtc(BinaryPrimitives.ReadInt64LittleEndian(valueBytes)),
ValueTypeInt32 when length == 4 => BinaryPrimitives.ReadInt32LittleEndian(valueBytes),
ValueTypeUtf16String when length >= 2 => DecodeUtf16String(valueBytes),
_ => valueBytes.ToArray()
};
afterOffset = valueStart + length;
return true;
}
private static string DecodeUtf16String(ReadOnlySpan valueBytes)
{
ushort charCount = BinaryPrimitives.ReadUInt16LittleEndian(valueBytes[..2]);
int byteCount = checked(charCount * 2);
if (byteCount > valueBytes.Length - 2)
{
byteCount = valueBytes.Length - 2;
}
return Encoding.Unicode.GetString(valueBytes.Slice(2, byteCount));
}
}