Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public enum MxDataType : short
|
||||
{
|
||||
Unknown = -1,
|
||||
NoData = 0,
|
||||
Boolean = 1,
|
||||
Integer = 2,
|
||||
Float = 3,
|
||||
Double = 4,
|
||||
String = 5,
|
||||
Time = 6,
|
||||
ElapsedTime = 7,
|
||||
ReferenceType = 8,
|
||||
StatusType = 9,
|
||||
Enum = 10,
|
||||
SecurityClassificationEnum = 11,
|
||||
DataQualityType = 12,
|
||||
QualifiedEnum = 13,
|
||||
QualifiedStruct = 14,
|
||||
InternationalizedString = 15,
|
||||
BigString = 16,
|
||||
End = 17,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public readonly record struct MxReferenceHandle(
|
||||
byte GalaxyId,
|
||||
ushort PlatformId,
|
||||
ushort EngineId,
|
||||
ushort ObjectId,
|
||||
ushort ObjectSignature,
|
||||
short PrimitiveId,
|
||||
short AttributeId,
|
||||
short PropertyId,
|
||||
ushort AttributeSignature,
|
||||
short AttributeIndex)
|
||||
{
|
||||
public const int EncodedLength = 20;
|
||||
private const ushort Crc16IbmPolynomial = 0xa001;
|
||||
|
||||
public ushort Reserved0 => 0;
|
||||
|
||||
public static MxReferenceHandle Create(
|
||||
byte galaxyId,
|
||||
ushort platformId,
|
||||
ushort engineId,
|
||||
ushort objectId,
|
||||
string objectTagName,
|
||||
short primitiveId,
|
||||
short attributeId,
|
||||
short propertyId,
|
||||
string attributeName,
|
||||
bool isArray)
|
||||
{
|
||||
return new MxReferenceHandle(
|
||||
galaxyId,
|
||||
platformId,
|
||||
engineId,
|
||||
objectId,
|
||||
ComputeNameSignature(objectTagName),
|
||||
primitiveId,
|
||||
attributeId,
|
||||
propertyId,
|
||||
ComputeNameSignature(attributeName),
|
||||
isArray ? (short)-1 : (short)0);
|
||||
}
|
||||
|
||||
public static ushort ComputeNameSignature(string name)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
|
||||
ushort crc = 0;
|
||||
foreach (char character in name.ToLowerInvariant())
|
||||
{
|
||||
crc = UpdateCrc16Ibm(crc, (byte)character);
|
||||
crc = UpdateCrc16Ibm(crc, (byte)(character >> 8));
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
public static MxReferenceHandle Parse(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
if (bytes.Length != EncodedLength)
|
||||
{
|
||||
throw new ArgumentException($"MX reference handle must be {EncodedLength} bytes.", nameof(bytes));
|
||||
}
|
||||
|
||||
return new MxReferenceHandle(
|
||||
GalaxyId: bytes[0],
|
||||
PlatformId: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(2, sizeof(ushort))),
|
||||
EngineId: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(4, sizeof(ushort))),
|
||||
ObjectId: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(6, sizeof(ushort))),
|
||||
ObjectSignature: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(8, sizeof(ushort))),
|
||||
PrimitiveId: BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(10, sizeof(short))),
|
||||
AttributeId: BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(12, sizeof(short))),
|
||||
PropertyId: BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(14, sizeof(short))),
|
||||
AttributeSignature: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(16, sizeof(ushort))),
|
||||
AttributeIndex: BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(18, sizeof(short))));
|
||||
}
|
||||
|
||||
public byte[] Encode()
|
||||
{
|
||||
byte[] bytes = new byte[EncodedLength];
|
||||
WriteTo(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public void WriteTo(Span<byte> destination)
|
||||
{
|
||||
if (destination.Length < EncodedLength)
|
||||
{
|
||||
throw new ArgumentException($"Destination must be at least {EncodedLength} bytes.", nameof(destination));
|
||||
}
|
||||
|
||||
destination[0] = GalaxyId;
|
||||
destination[1] = 0;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(2, sizeof(ushort)), PlatformId);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(4, sizeof(ushort)), EngineId);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(6, sizeof(ushort)), ObjectId);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(8, sizeof(ushort)), ObjectSignature);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination.Slice(10, sizeof(short)), PrimitiveId);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination.Slice(12, sizeof(short)), AttributeId);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination.Slice(14, sizeof(short)), PropertyId);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(16, sizeof(ushort)), AttributeSignature);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination.Slice(18, sizeof(short)), AttributeIndex);
|
||||
}
|
||||
|
||||
private static ushort UpdateCrc16Ibm(ushort crc, byte value)
|
||||
{
|
||||
crc ^= value;
|
||||
for (int bit = 0; bit < 8; bit++)
|
||||
{
|
||||
crc = (ushort)(((crc & 1) != 0)
|
||||
? ((crc >> 1) ^ Crc16IbmPolynomial)
|
||||
: (crc >> 1));
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public enum MxStatusCategory : short
|
||||
{
|
||||
Unknown = -1,
|
||||
Ok = 0,
|
||||
Pending = 1,
|
||||
Warning = 2,
|
||||
CommunicationError = 3,
|
||||
ConfigurationError = 4,
|
||||
OperationalError = 5,
|
||||
SecurityError = 6,
|
||||
SoftwareError = 7,
|
||||
OtherError = 8,
|
||||
}
|
||||
|
||||
public enum MxStatusSource : short
|
||||
{
|
||||
Unknown = -1,
|
||||
RequestingLmx = 0,
|
||||
RespondingLmx = 1,
|
||||
RequestingNmx = 2,
|
||||
RespondingNmx = 3,
|
||||
RequestingAutomationObject = 4,
|
||||
RespondingAutomationObject = 5,
|
||||
}
|
||||
|
||||
public sealed record MxStatus(
|
||||
short Success,
|
||||
MxStatusCategory Category,
|
||||
MxStatusSource DetectedBy,
|
||||
short Detail)
|
||||
{
|
||||
public string? DetailText => MxStatusDetails.GetKnownText(Detail);
|
||||
|
||||
public static MxStatus DataChangeOk { get; } = new(
|
||||
Success: -1,
|
||||
Category: MxStatusCategory.Ok,
|
||||
DetectedBy: MxStatusSource.RequestingLmx,
|
||||
Detail: 0);
|
||||
|
||||
public static MxStatus WriteCompleteOk { get; } = new(
|
||||
Success: -1,
|
||||
Category: MxStatusCategory.Ok,
|
||||
DetectedBy: MxStatusSource.RespondingAutomationObject,
|
||||
Detail: 0);
|
||||
|
||||
public static MxStatus SuspendPending { get; } = new(
|
||||
Success: -1,
|
||||
Category: MxStatusCategory.Pending,
|
||||
DetectedBy: MxStatusSource.RequestingLmx,
|
||||
Detail: 0);
|
||||
|
||||
public static MxStatus ActivateOk { get; } = new(
|
||||
Success: -1,
|
||||
Category: MxStatusCategory.Ok,
|
||||
DetectedBy: MxStatusSource.RequestingLmx,
|
||||
Detail: 0);
|
||||
|
||||
public static MxStatus InvalidReferenceConfiguration { get; } = new(
|
||||
Success: 0,
|
||||
Category: MxStatusCategory.ConfigurationError,
|
||||
DetectedBy: MxStatusSource.RequestingLmx,
|
||||
Detail: 6);
|
||||
}
|
||||
|
||||
public static class MxStatusDetails
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<short, string> KnownDetails = new Dictionary<short, string>
|
||||
{
|
||||
[16] = "Request timed out",
|
||||
[17] = "Platform communication error",
|
||||
[18] = "Invalid platform ID",
|
||||
[19] = "Invalid engine ID",
|
||||
[20] = "Engine communication error",
|
||||
[21] = "Invalid reference",
|
||||
[22] = "No Galaxy Repository",
|
||||
[23] = "Invalid object ID",
|
||||
[24] = "Object signature mismatch",
|
||||
[25] = "Invalid primitive ID",
|
||||
[26] = "Invalid attribute ID",
|
||||
[27] = "Invalid property ID",
|
||||
[28] = "Index out of range",
|
||||
[29] = "Data out of range",
|
||||
[30] = "Incorrect data type",
|
||||
[31] = "Attribute not readable",
|
||||
[32] = "Attribute not writeable",
|
||||
[33] = "Write access denied",
|
||||
[34] = "Unknown error",
|
||||
[35] = "detected by",
|
||||
[36] = "Wrong data type",
|
||||
[37] = "Wrong number of dimensions",
|
||||
[38] = "Invalid index",
|
||||
[39] = "Index out of order",
|
||||
[40] = "Dimension does not exist",
|
||||
[41] = "Conversion not supported",
|
||||
[42] = "Unable to convert string",
|
||||
[43] = "Overflow",
|
||||
[44] = "Attribute signature mismatch",
|
||||
[45] = "Resolving local portion of reference",
|
||||
[46] = "Resolving global portion of reference",
|
||||
[47] = "Nmx version mismatch",
|
||||
[48] = "Nmx command not valid",
|
||||
[49] = "Lmx version mismatch",
|
||||
[50] = "Lmx command not valid",
|
||||
[51] = "However, the object could not be put On Scan - Permission to modify \"Operate\" attributes is required",
|
||||
[52] = "Unable to resolve reference for 'set' request because Galaxy Repository is busy performing a 'Deploy/Undeploy' operation",
|
||||
[53] = "Too many outstanding pending requests to engine",
|
||||
[54] = "Object Initializing",
|
||||
[55] = "Engine Initializing",
|
||||
[56] = "Secured Write",
|
||||
[57] = "Verified Write",
|
||||
[58] = "No Alarm Ack Privilege",
|
||||
[59] = "Alarm Acked Already",
|
||||
[60] = "User did not have the necessary permissions to write",
|
||||
[61] = "Verifier did not have the necessary permissions to verify",
|
||||
[541] = "Conversion to intended data type is not supported",
|
||||
[542] = "Unable to convert the input string to intended data type",
|
||||
[8017] = "Object must be offscan to modify attributes that have an MxSecurityConfigure security classification",
|
||||
};
|
||||
|
||||
public static string? GetKnownText(short detail)
|
||||
{
|
||||
return KnownDetails.TryGetValue(detail, out string? text) ? text : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public enum MxValueKind
|
||||
{
|
||||
Boolean,
|
||||
Int32,
|
||||
Float32,
|
||||
Float64,
|
||||
String,
|
||||
DateTime,
|
||||
ElapsedTime,
|
||||
BooleanArray,
|
||||
Int32Array,
|
||||
Float32Array,
|
||||
Float64Array,
|
||||
StringArray,
|
||||
DateTimeArray,
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public enum NmxItemControlCommand : byte
|
||||
{
|
||||
Advise = 0x1f,
|
||||
AdviseSupervisory = 0x1f,
|
||||
UnAdvise = 0x21,
|
||||
}
|
||||
|
||||
public sealed record NmxItemControlMessage(
|
||||
NmxItemControlCommand Command,
|
||||
Guid ItemCorrelationId,
|
||||
ushort ObjectId,
|
||||
ushort ObjectSignature,
|
||||
short PrimitiveId,
|
||||
short AttributeId,
|
||||
short PropertyId,
|
||||
ushort AttributeSignature,
|
||||
short AttributeIndex,
|
||||
uint Tail)
|
||||
{
|
||||
private const ushort Version = 1;
|
||||
private const int HeaderLength = 3;
|
||||
private const int GuidLength = 16;
|
||||
private const int AdviseExtraLength = 2;
|
||||
private const int PayloadLength = 18;
|
||||
|
||||
public static int GetEncodedLength(NmxItemControlCommand command)
|
||||
{
|
||||
return HeaderLength
|
||||
+ GuidLength
|
||||
+ (command == NmxItemControlCommand.AdviseSupervisory ? AdviseExtraLength : 0)
|
||||
+ PayloadLength;
|
||||
}
|
||||
|
||||
public static NmxItemControlMessage Parse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < HeaderLength + GuidLength + PayloadLength)
|
||||
{
|
||||
throw new ArgumentException("NMX item-control body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
var command = (NmxItemControlCommand)body[0];
|
||||
if (command is not (NmxItemControlCommand.AdviseSupervisory or NmxItemControlCommand.UnAdvise))
|
||||
{
|
||||
throw new ArgumentException($"Unsupported item-control command 0x{body[0]:X2}.", nameof(body));
|
||||
}
|
||||
|
||||
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(1, sizeof(ushort)));
|
||||
if (version != Version)
|
||||
{
|
||||
throw new ArgumentException($"Unsupported item-control version {version}.", nameof(body));
|
||||
}
|
||||
|
||||
int expectedLength = GetEncodedLength(command);
|
||||
if (body.Length != expectedLength)
|
||||
{
|
||||
throw new ArgumentException($"Unexpected item-control body length {body.Length}; expected {expectedLength}.", nameof(body));
|
||||
}
|
||||
|
||||
int offset = HeaderLength;
|
||||
var itemCorrelationId = new Guid(body.Slice(offset, GuidLength));
|
||||
offset += GuidLength;
|
||||
if (command == NmxItemControlCommand.AdviseSupervisory)
|
||||
{
|
||||
offset += AdviseExtraLength;
|
||||
}
|
||||
|
||||
return new NmxItemControlMessage(
|
||||
command,
|
||||
itemCorrelationId,
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(offset, sizeof(ushort))),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(offset + 2, sizeof(ushort))),
|
||||
BinaryPrimitives.ReadInt16LittleEndian(body.Slice(offset + 4, sizeof(short))),
|
||||
BinaryPrimitives.ReadInt16LittleEndian(body.Slice(offset + 6, sizeof(short))),
|
||||
BinaryPrimitives.ReadInt16LittleEndian(body.Slice(offset + 8, sizeof(short))),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(offset + 10, sizeof(ushort))),
|
||||
BinaryPrimitives.ReadInt16LittleEndian(body.Slice(offset + 12, sizeof(short))),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(offset + 14, sizeof(uint))));
|
||||
}
|
||||
|
||||
public static NmxItemControlMessage FromReferenceHandle(
|
||||
NmxItemControlCommand command,
|
||||
Guid itemCorrelationId,
|
||||
MxReferenceHandle referenceHandle,
|
||||
uint tail = 3)
|
||||
{
|
||||
return new NmxItemControlMessage(
|
||||
command,
|
||||
itemCorrelationId,
|
||||
referenceHandle.ObjectId,
|
||||
referenceHandle.ObjectSignature,
|
||||
referenceHandle.PrimitiveId,
|
||||
referenceHandle.AttributeId,
|
||||
referenceHandle.PropertyId,
|
||||
referenceHandle.AttributeSignature,
|
||||
referenceHandle.AttributeIndex,
|
||||
tail);
|
||||
}
|
||||
|
||||
public MxReferenceHandle ToReferenceHandle(
|
||||
byte galaxyId = 1,
|
||||
ushort platformId = 1,
|
||||
ushort engineId = 1)
|
||||
{
|
||||
return new MxReferenceHandle(
|
||||
galaxyId,
|
||||
platformId,
|
||||
engineId,
|
||||
ObjectId,
|
||||
ObjectSignature,
|
||||
PrimitiveId,
|
||||
AttributeId,
|
||||
PropertyId,
|
||||
AttributeSignature,
|
||||
AttributeIndex);
|
||||
}
|
||||
|
||||
public byte[] Encode()
|
||||
{
|
||||
byte[] body = new byte[GetEncodedLength(Command)];
|
||||
body[0] = (byte)Command;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(1, sizeof(ushort)), Version);
|
||||
int offset = HeaderLength;
|
||||
ItemCorrelationId.TryWriteBytes(body.AsSpan(offset, GuidLength));
|
||||
offset += GuidLength;
|
||||
if (Command == NmxItemControlCommand.AdviseSupervisory)
|
||||
{
|
||||
offset += AdviseExtraLength;
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(offset, sizeof(ushort)), ObjectId);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(offset + 2, sizeof(ushort)), ObjectSignature);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset + 4, sizeof(short)), PrimitiveId);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset + 6, sizeof(short)), AttributeId);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset + 8, sizeof(short)), PropertyId);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(offset + 10, sizeof(ushort)), AttributeSignature);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset + 12, sizeof(short)), AttributeIndex);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(offset + 14, sizeof(uint)), Tail);
|
||||
return body;
|
||||
}
|
||||
|
||||
public NmxItemControlMessage ToUnAdvise()
|
||||
{
|
||||
return this with { Command = NmxItemControlCommand.UnAdvise };
|
||||
}
|
||||
|
||||
public NmxItemControlMessage ToAdviseSupervisory()
|
||||
{
|
||||
return this with { Command = NmxItemControlCommand.AdviseSupervisory };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public static class NmxMetadataQueryMessage
|
||||
{
|
||||
private const int PreAdviseCorrelationOffset = 0x8a;
|
||||
|
||||
public static byte[] EncodeObservedPreAdvise(Guid itemCorrelationId)
|
||||
{
|
||||
byte[] body = Convert.FromHexString(
|
||||
"17010001010001000000650071000a0000000000086a0000004000008144006500760050006c006100740066006f0072006d002e00470052002e00540069006d0065004f0066004c006100730074004400650070006c006f00790000000200000000000200000000000200000000000101000100010000000000000000000000000001d0fc40091f0100c0ca9ccd3265b046a585a583b2e77a5d000001000000" +
|
||||
"17010001010001000000650071000a000000000008760000004c00008144006500760050006c006100740066006f0072006d002e00470052002e00540069006d0065004f0066004c0061007300740043006f006e006600690067004300680061006e0067006500000002000000000002000000000002000000000001010001000100000000000000000000000000015003410920010002000000");
|
||||
itemCorrelationId.TryWriteBytes(body.AsSpan(PreAdviseCorrelationOffset, 16));
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public sealed record NmxObservedEnvelope(
|
||||
bool HasLengthPrefix,
|
||||
int? TotalLengthPrefix,
|
||||
int DeclaredInnerLength,
|
||||
int ActualInnerLength,
|
||||
ReadOnlyMemory<byte> Header,
|
||||
ReadOnlyMemory<byte> InnerBody)
|
||||
{
|
||||
public const int HeaderLength = 46;
|
||||
public const int InnerLengthOffset = 2;
|
||||
|
||||
public static NmxObservedEnvelope ParseTransferDataBody(ReadOnlyMemory<byte> body)
|
||||
{
|
||||
if (body.Length < HeaderLength)
|
||||
{
|
||||
throw new ArgumentException("NMX TransferData body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
int declaredInnerLength = BinaryPrimitives.ReadInt32LittleEndian(body.Span.Slice(InnerLengthOffset, sizeof(int)));
|
||||
int actualInnerLength = body.Length - HeaderLength;
|
||||
if (declaredInnerLength != actualInnerLength)
|
||||
{
|
||||
throw new ArgumentException("NMX TransferData inner length does not match body size.", nameof(body));
|
||||
}
|
||||
|
||||
return new NmxObservedEnvelope(
|
||||
false,
|
||||
null,
|
||||
declaredInnerLength,
|
||||
actualInnerLength,
|
||||
body[..HeaderLength],
|
||||
body.Slice(HeaderLength, actualInnerLength));
|
||||
}
|
||||
|
||||
public static NmxObservedEnvelope ParseProcessDataReceivedBody(ReadOnlyMemory<byte> body)
|
||||
{
|
||||
if (body.Length < sizeof(int) + HeaderLength)
|
||||
{
|
||||
throw new ArgumentException("NMX ProcessDataReceived body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
int totalLengthPrefix = BinaryPrimitives.ReadInt32LittleEndian(body.Span[..sizeof(int)]);
|
||||
if (totalLengthPrefix != body.Length)
|
||||
{
|
||||
throw new ArgumentException("NMX ProcessDataReceived length prefix does not match body size.", nameof(body));
|
||||
}
|
||||
|
||||
int headerOffset = sizeof(int);
|
||||
int declaredInnerLength = BinaryPrimitives.ReadInt32LittleEndian(
|
||||
body.Span.Slice(headerOffset + InnerLengthOffset, sizeof(int)));
|
||||
int actualInnerLength = declaredInnerLength - sizeof(int);
|
||||
if (actualInnerLength < 0 || headerOffset + HeaderLength + actualInnerLength != body.Length)
|
||||
{
|
||||
throw new ArgumentException("NMX ProcessDataReceived inner length does not match body size.", nameof(body));
|
||||
}
|
||||
|
||||
return new NmxObservedEnvelope(
|
||||
true,
|
||||
totalLengthPrefix,
|
||||
declaredInnerLength,
|
||||
actualInnerLength,
|
||||
body.Slice(headerOffset, HeaderLength),
|
||||
body.Slice(headerOffset + HeaderLength, actualInnerLength));
|
||||
}
|
||||
|
||||
public static NmxObservedEnvelope ParseProcessDataReceivedBodyFlexible(ReadOnlyMemory<byte> body)
|
||||
{
|
||||
if (body.Length >= sizeof(int) + HeaderLength)
|
||||
{
|
||||
int totalLengthPrefix = BinaryPrimitives.ReadInt32LittleEndian(body.Span[..sizeof(int)]);
|
||||
if (totalLengthPrefix == body.Length)
|
||||
{
|
||||
return ParseProcessDataReceivedBody(body);
|
||||
}
|
||||
}
|
||||
|
||||
if (body.Length < HeaderLength)
|
||||
{
|
||||
throw new ArgumentException("NMX ProcessDataReceived body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
int declaredInnerLength = BinaryPrimitives.ReadInt32LittleEndian(body.Span.Slice(InnerLengthOffset, sizeof(int)));
|
||||
int actualInnerLength = body.Length - HeaderLength;
|
||||
if (declaredInnerLength != actualInnerLength)
|
||||
{
|
||||
throw new ArgumentException("NMX ProcessDataReceived header inner length does not match body size.", nameof(body));
|
||||
}
|
||||
|
||||
return new NmxObservedEnvelope(
|
||||
false,
|
||||
null,
|
||||
declaredInnerLength,
|
||||
actualInnerLength,
|
||||
body[..HeaderLength],
|
||||
body.Slice(HeaderLength, actualInnerLength));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record NmxObservedString(int Offset, string Value);
|
||||
|
||||
public sealed record NmxObservedMessage(
|
||||
byte Command,
|
||||
string CommandName,
|
||||
byte VersionMajor,
|
||||
byte VersionMinor,
|
||||
Guid? ItemCorrelationId,
|
||||
IReadOnlyList<NmxObservedString> Strings)
|
||||
{
|
||||
public static NmxObservedMessage Parse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < 3)
|
||||
{
|
||||
throw new ArgumentException("NMX message body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
byte command = body[0];
|
||||
Guid? itemCorrelationId = null;
|
||||
if (command is 0x1f or 0x21 && body.Length >= 19)
|
||||
{
|
||||
itemCorrelationId = new Guid(body.Slice(3, 16));
|
||||
}
|
||||
|
||||
return new NmxObservedMessage(
|
||||
command,
|
||||
GetCommandName(command),
|
||||
body[1],
|
||||
body[2],
|
||||
itemCorrelationId,
|
||||
ExtractUtf16Strings(body));
|
||||
}
|
||||
|
||||
private static string GetCommandName(byte command)
|
||||
{
|
||||
return command switch
|
||||
{
|
||||
0x17 => "MetadataQuery",
|
||||
0x1f => "AdviseSupervisory",
|
||||
0x21 => "UnAdvise",
|
||||
0x32 => "SubscriptionStatus",
|
||||
0x33 => "DataUpdate",
|
||||
0x37 => "Write",
|
||||
0x40 => "MetadataResponse",
|
||||
_ => $"Unknown0x{command:X2}",
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<NmxObservedString> ExtractUtf16Strings(ReadOnlySpan<byte> body)
|
||||
{
|
||||
List<NmxObservedString> strings = [];
|
||||
int offset = 0;
|
||||
while (offset + 8 <= body.Length)
|
||||
{
|
||||
int start = offset;
|
||||
int chars = 0;
|
||||
while (offset + 1 < body.Length)
|
||||
{
|
||||
byte lo = body[offset];
|
||||
byte hi = body[offset + 1];
|
||||
if (lo == 0 && hi == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (hi != 0 || lo < 0x20 || lo > 0x7e)
|
||||
{
|
||||
chars = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
chars++;
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
if (chars >= 3 && offset + 1 < body.Length && body[offset] == 0 && body[offset + 1] == 0)
|
||||
{
|
||||
string value = Encoding.Unicode.GetString(body.Slice(start, chars * 2));
|
||||
strings.Add(new NmxObservedString(start, value));
|
||||
offset += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
offset = start + 1;
|
||||
}
|
||||
|
||||
return strings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public enum NmxOperationStatusFormat
|
||||
{
|
||||
CompletionOnly,
|
||||
StatusWord,
|
||||
}
|
||||
|
||||
public sealed record NmxOperationStatusMessage(
|
||||
NmxOperationStatusFormat Format,
|
||||
byte Command,
|
||||
ushort StatusCode,
|
||||
byte CompletionCode,
|
||||
MxStatus Status)
|
||||
{
|
||||
public bool IsMxAccessWriteComplete => Format == NmxOperationStatusFormat.StatusWord
|
||||
&& StatusCode == 0x8050
|
||||
&& CompletionCode == 0x00;
|
||||
|
||||
public static bool TryParseProcessDataReceivedBody(ReadOnlyMemory<byte> body, out NmxOperationStatusMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var envelope = NmxObservedEnvelope.ParseProcessDataReceivedBodyFlexible(body);
|
||||
return TryParseInner(envelope.InnerBody.Span, out message);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
message = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParseInner(ReadOnlySpan<byte> inner, out NmxOperationStatusMessage message)
|
||||
{
|
||||
if (inner.Length == 1)
|
||||
{
|
||||
byte completionCode = inner[0];
|
||||
message = new NmxOperationStatusMessage(
|
||||
Format: NmxOperationStatusFormat.CompletionOnly,
|
||||
Command: 0,
|
||||
StatusCode: 0,
|
||||
CompletionCode: completionCode,
|
||||
Status: CreateUnpromotedCompletionStatus(completionCode));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (inner.Length == 5 && inner[0] == 0x00 && inner[1] == 0x00)
|
||||
{
|
||||
ushort statusCode = (ushort)(inner[2] | (inner[3] << 8));
|
||||
byte completionCode = inner[4];
|
||||
message = new NmxOperationStatusMessage(
|
||||
Format: NmxOperationStatusFormat.StatusWord,
|
||||
Command: inner[0],
|
||||
StatusCode: statusCode,
|
||||
CompletionCode: completionCode,
|
||||
Status: statusCode == 0x8050 && completionCode == 0x00 ? MxStatus.WriteCompleteOk : new MxStatus(
|
||||
Success: 0,
|
||||
Category: MxStatusCategory.Unknown,
|
||||
DetectedBy: MxStatusSource.Unknown,
|
||||
Detail: completionCode == 0x00 ? unchecked((short)statusCode) : completionCode));
|
||||
return true;
|
||||
}
|
||||
|
||||
message = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static MxStatus CreateUnpromotedCompletionStatus(byte completionCode)
|
||||
{
|
||||
return new MxStatus(
|
||||
Success: 0,
|
||||
Category: MxStatusCategory.Unknown,
|
||||
DetectedBy: MxStatusSource.Unknown,
|
||||
Detail: completionCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public sealed record NmxReferenceRegistrationMessage(
|
||||
int ItemHandle,
|
||||
Guid ItemCorrelationId,
|
||||
string ItemDefinition,
|
||||
string ItemContext,
|
||||
bool Subscribe)
|
||||
{
|
||||
private const byte Command = 0x10;
|
||||
private const ushort Version = 1;
|
||||
private const int HeaderLength = 55;
|
||||
private const int ItemStringReservedLength = 8;
|
||||
private const int TailLength = 20;
|
||||
|
||||
public static NmxReferenceRegistrationMessage Parse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < HeaderLength + ItemStringReservedLength + TailLength)
|
||||
{
|
||||
throw new ArgumentException("NMX reference-registration body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
if (body[0] != Command)
|
||||
{
|
||||
throw new ArgumentException($"Unsupported reference-registration command 0x{body[0]:X2}.", nameof(body));
|
||||
}
|
||||
|
||||
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(1, sizeof(ushort)));
|
||||
if (version != Version)
|
||||
{
|
||||
throw new ArgumentException($"Unsupported reference-registration version {version}.", nameof(body));
|
||||
}
|
||||
|
||||
int itemHandle = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(3, sizeof(int)));
|
||||
var correlationId = new Guid(body.Slice(7, 16));
|
||||
int offset = HeaderLength;
|
||||
string itemDefinition = ReadRegisteredString(body, ref offset, taggedLength: true);
|
||||
|
||||
if (body.Slice(offset, ItemStringReservedLength).IndexOfAnyExcept((byte)0) >= 0)
|
||||
{
|
||||
throw new ArgumentException("Unexpected nonzero reference-registration item string reserved bytes.", nameof(body));
|
||||
}
|
||||
|
||||
offset += ItemStringReservedLength;
|
||||
string itemContext = ReadRegisteredString(body, ref offset, taggedLength: false);
|
||||
if (body.Length - offset != TailLength)
|
||||
{
|
||||
throw new ArgumentException($"Unexpected reference-registration tail length {body.Length - offset}.", nameof(body));
|
||||
}
|
||||
|
||||
if (body.Slice(offset, TailLength - 1).IndexOfAnyExcept((byte)0) >= 0)
|
||||
{
|
||||
throw new ArgumentException("Unexpected nonzero reference-registration tail bytes.", nameof(body));
|
||||
}
|
||||
|
||||
return new NmxReferenceRegistrationMessage(
|
||||
itemHandle,
|
||||
correlationId,
|
||||
itemDefinition,
|
||||
itemContext,
|
||||
body[offset + TailLength - 1] != 0);
|
||||
}
|
||||
|
||||
public byte[] Encode()
|
||||
{
|
||||
byte[] itemDefinitionBytes = EncodeNullTerminatedUtf16(ItemDefinition);
|
||||
byte[] itemContextBytes = EncodeNullTerminatedUtf16(ItemContext);
|
||||
byte[] body = new byte[
|
||||
HeaderLength
|
||||
+ sizeof(int)
|
||||
+ itemDefinitionBytes.Length
|
||||
+ ItemStringReservedLength
|
||||
+ sizeof(int)
|
||||
+ itemContextBytes.Length
|
||||
+ TailLength];
|
||||
|
||||
body[0] = Command;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(1, sizeof(ushort)), Version);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(3, sizeof(int)), ItemHandle);
|
||||
ItemCorrelationId.TryWriteBytes(body.AsSpan(7, 16));
|
||||
|
||||
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(23, sizeof(short)), -1);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(27, sizeof(int)), 1);
|
||||
|
||||
int offset = HeaderLength;
|
||||
WriteRegisteredString(body, ref offset, itemDefinitionBytes, taggedLength: true);
|
||||
offset += ItemStringReservedLength;
|
||||
WriteRegisteredString(body, ref offset, itemContextBytes, taggedLength: false);
|
||||
body[offset + TailLength - 1] = Subscribe ? (byte)1 : (byte)0;
|
||||
return body;
|
||||
}
|
||||
|
||||
public static string ToBufferedItemDefinition(string itemDefinition)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
||||
return itemDefinition.EndsWith(".property(buffer)", StringComparison.OrdinalIgnoreCase)
|
||||
? itemDefinition
|
||||
: itemDefinition + ".property(buffer)";
|
||||
}
|
||||
|
||||
private static string ReadRegisteredString(ReadOnlySpan<byte> body, ref int offset, bool taggedLength)
|
||||
{
|
||||
int rawLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
|
||||
int byteLength = taggedLength ? rawLength & 0x00FF_FFFF : rawLength;
|
||||
if (taggedLength && (rawLength & unchecked((int)0xFF00_0000)) != unchecked((int)0x8100_0000))
|
||||
{
|
||||
throw new ArgumentException("Reference-registration item definition is missing the observed 0x81 string marker.");
|
||||
}
|
||||
|
||||
offset += sizeof(int);
|
||||
if (byteLength < 2 || byteLength % 2 != 0 || offset + byteLength > body.Length)
|
||||
{
|
||||
throw new ArgumentException($"Invalid reference-registration string byte length {byteLength}.");
|
||||
}
|
||||
|
||||
string value = Encoding.Unicode.GetString(body.Slice(offset, byteLength - 2));
|
||||
if (body[offset + byteLength - 2] != 0 || body[offset + byteLength - 1] != 0)
|
||||
{
|
||||
throw new ArgumentException("Reference-registration string is not null terminated.");
|
||||
}
|
||||
|
||||
offset += byteLength;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void WriteRegisteredString(byte[] body, ref int offset, byte[] value, bool taggedLength)
|
||||
{
|
||||
int rawLength = taggedLength ? value.Length | unchecked((int)0x8100_0000) : value.Length;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(offset, sizeof(int)), rawLength);
|
||||
offset += sizeof(int);
|
||||
value.CopyTo(body.AsSpan(offset));
|
||||
offset += value.Length;
|
||||
}
|
||||
|
||||
private static byte[] EncodeNullTerminatedUtf16(string value)
|
||||
{
|
||||
return Encoding.Unicode.GetBytes(value + '\0');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public sealed record NmxReferenceRegistrationResultMessage(
|
||||
int ItemHandle,
|
||||
Guid ItemCorrelationId,
|
||||
DateTime FirstTimestampUtc,
|
||||
DateTime SecondTimestampUtc,
|
||||
byte StatusCategory,
|
||||
byte StatusDetail,
|
||||
string ItemDefinition,
|
||||
int MxDataType,
|
||||
string ItemContext)
|
||||
{
|
||||
private const byte Command = 0x11;
|
||||
private const ushort Version = 1;
|
||||
private const int HeaderLength = 45;
|
||||
private const int BetweenStringsLength = 10;
|
||||
private const int TailLength = 16;
|
||||
|
||||
public static NmxReferenceRegistrationResultMessage Parse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < HeaderLength + TailLength)
|
||||
{
|
||||
throw new ArgumentException("NMX reference-registration result body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
if (body[0] != Command)
|
||||
{
|
||||
throw new ArgumentException($"Unsupported reference-registration result command 0x{body[0]:X2}.", nameof(body));
|
||||
}
|
||||
|
||||
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(1, sizeof(ushort)));
|
||||
if (version != Version)
|
||||
{
|
||||
throw new ArgumentException($"Unsupported reference-registration result version {version}.", nameof(body));
|
||||
}
|
||||
|
||||
int blockLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(41, sizeof(int)));
|
||||
if (blockLength != body.Length - 41)
|
||||
{
|
||||
throw new ArgumentException("Reference-registration result block length does not match body size.", nameof(body));
|
||||
}
|
||||
|
||||
int offset = HeaderLength;
|
||||
string itemDefinition = ReadRegisteredString(body, ref offset, taggedLength: true);
|
||||
int mxDataType = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
|
||||
offset += sizeof(int);
|
||||
|
||||
if (body.Slice(offset, BetweenStringsLength - sizeof(int)).IndexOfAnyExcept((byte)0) >= 0)
|
||||
{
|
||||
throw new ArgumentException("Unexpected nonzero reference-registration result reserved bytes.", nameof(body));
|
||||
}
|
||||
|
||||
offset += BetweenStringsLength - sizeof(int);
|
||||
string itemContext = ReadRegisteredString(body, ref offset, taggedLength: false);
|
||||
if (body.Length - offset != TailLength)
|
||||
{
|
||||
throw new ArgumentException($"Unexpected reference-registration result tail length {body.Length - offset}.", nameof(body));
|
||||
}
|
||||
|
||||
if (body.Slice(offset, TailLength).IndexOfAnyExcept((byte)0) >= 0)
|
||||
{
|
||||
throw new ArgumentException("Unexpected nonzero reference-registration result tail bytes.", nameof(body));
|
||||
}
|
||||
|
||||
return new NmxReferenceRegistrationResultMessage(
|
||||
BinaryPrimitives.ReadInt32LittleEndian(body.Slice(3, sizeof(int))),
|
||||
new Guid(body.Slice(7, 16)),
|
||||
DateTime.FromFileTimeUtc(BinaryPrimitives.ReadInt64LittleEndian(body.Slice(23, sizeof(long)))),
|
||||
DateTime.FromFileTimeUtc(BinaryPrimitives.ReadInt64LittleEndian(body.Slice(31, sizeof(long)))),
|
||||
body[39],
|
||||
body[40],
|
||||
itemDefinition,
|
||||
mxDataType,
|
||||
itemContext);
|
||||
}
|
||||
|
||||
public static bool TryParseProcessDataReceivedBody(ReadOnlyMemory<byte> processDataBody, out NmxReferenceRegistrationResultMessage? message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var envelope = NmxObservedEnvelope.ParseProcessDataReceivedBodyFlexible(processDataBody);
|
||||
message = Parse(envelope.InnerBody.Span);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadRegisteredString(ReadOnlySpan<byte> body, ref int offset, bool taggedLength)
|
||||
{
|
||||
int rawLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
|
||||
int byteLength = taggedLength ? rawLength & 0x00FF_FFFF : rawLength;
|
||||
if (taggedLength && (rawLength & unchecked((int)0xFF00_0000)) != unchecked((int)0x8100_0000))
|
||||
{
|
||||
throw new ArgumentException("Reference-registration result item definition is missing the observed 0x81 string marker.");
|
||||
}
|
||||
|
||||
offset += sizeof(int);
|
||||
if (byteLength < 2 || byteLength % 2 != 0 || offset + byteLength > body.Length)
|
||||
{
|
||||
throw new ArgumentException($"Invalid reference-registration result string byte length {byteLength}.");
|
||||
}
|
||||
|
||||
string value = Encoding.Unicode.GetString(body.Slice(offset, byteLength - 2));
|
||||
if (body[offset + byteLength - 2] != 0 || body[offset + byteLength - 1] != 0)
|
||||
{
|
||||
throw new ArgumentException("Reference-registration result string is not null terminated.");
|
||||
}
|
||||
|
||||
offset += byteLength;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public static class NmxSecuredWrite2Message
|
||||
{
|
||||
public const byte Command = 0x38;
|
||||
public const ushort Version = 1;
|
||||
public const int AuthenticatorTokenLength = 16;
|
||||
|
||||
public static readonly byte[] ObservedAuthenticatedUserToken =
|
||||
[
|
||||
0x07, 0xb9, 0xa9, 0xf4,
|
||||
0x72, 0x6e, 0xae, 0x48,
|
||||
0x83, 0xb5, 0xbb, 0xde,
|
||||
0x91, 0x8c, 0x89, 0x0f,
|
||||
];
|
||||
|
||||
public static byte[] Encode(
|
||||
MxReferenceHandle referenceHandle,
|
||||
MxValueKind valueKind,
|
||||
object value,
|
||||
DateTime timestamp,
|
||||
string clientName,
|
||||
ReadOnlySpan<byte> currentUserToken,
|
||||
ReadOnlySpan<byte> verifierUserToken,
|
||||
int writeIndex = 1,
|
||||
uint clientToken = 0)
|
||||
{
|
||||
if (currentUserToken.Length != AuthenticatorTokenLength)
|
||||
{
|
||||
throw new ArgumentException("Current user token must be 16 bytes.", nameof(currentUserToken));
|
||||
}
|
||||
|
||||
if (verifierUserToken.Length != AuthenticatorTokenLength)
|
||||
{
|
||||
throw new ArgumentException("Verifier user token must be 16 bytes.", nameof(verifierUserToken));
|
||||
}
|
||||
|
||||
byte[] timestampedPrefix = NmxWriteMessage.EncodeTimestamped(
|
||||
referenceHandle,
|
||||
valueKind,
|
||||
value,
|
||||
timestamp,
|
||||
writeIndex,
|
||||
clientToken: 0);
|
||||
timestampedPrefix[0] = Command;
|
||||
|
||||
byte[] clientNameBytes = Encoding.Unicode.GetBytes((clientName ?? string.Empty) + '\0');
|
||||
int prefixLength = timestampedPrefix.Length - sizeof(uint) - sizeof(int);
|
||||
byte[] body = new byte[prefixLength + AuthenticatorTokenLength + sizeof(int) + clientNameBytes.Length + AuthenticatorTokenLength + sizeof(short) + sizeof(uint) + sizeof(int)];
|
||||
|
||||
timestampedPrefix.AsSpan(0, prefixLength).CopyTo(body);
|
||||
int offset = prefixLength;
|
||||
currentUserToken.CopyTo(body.AsSpan(offset, AuthenticatorTokenLength));
|
||||
offset += AuthenticatorTokenLength;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(offset, sizeof(int)), clientNameBytes.Length);
|
||||
offset += sizeof(int);
|
||||
clientNameBytes.CopyTo(body.AsSpan(offset));
|
||||
offset += clientNameBytes.Length;
|
||||
verifierUserToken.CopyTo(body.AsSpan(offset, AuthenticatorTokenLength));
|
||||
offset += AuthenticatorTokenLength;
|
||||
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset, sizeof(short)), -1);
|
||||
offset += sizeof(short);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(offset, sizeof(uint)), clientToken);
|
||||
offset += sizeof(uint);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(offset, sizeof(int)), writeIndex);
|
||||
return body;
|
||||
}
|
||||
|
||||
public static byte[] EncodeBoolean(
|
||||
MxReferenceHandle referenceHandle,
|
||||
bool value,
|
||||
DateTime timestamp,
|
||||
string clientName,
|
||||
ReadOnlySpan<byte> currentUserToken,
|
||||
ReadOnlySpan<byte> verifierUserToken,
|
||||
int writeIndex = 1,
|
||||
uint clientToken = 0)
|
||||
{
|
||||
return Encode(
|
||||
referenceHandle,
|
||||
MxValueKind.Boolean,
|
||||
value,
|
||||
timestamp,
|
||||
clientName,
|
||||
currentUserToken,
|
||||
verifierUserToken,
|
||||
writeIndex,
|
||||
clientToken);
|
||||
}
|
||||
|
||||
public static byte[] ResolveObservedUserToken(int userId)
|
||||
{
|
||||
return userId == 0
|
||||
? new byte[AuthenticatorTokenLength]
|
||||
: ObservedAuthenticatedUserToken.ToArray();
|
||||
}
|
||||
|
||||
public static string FormatToken(ReadOnlySpan<byte> token)
|
||||
{
|
||||
return Convert.ToHexString(token).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public sealed record NmxCallbackValue(
|
||||
byte WireKind,
|
||||
MxValueKind? ValueKind,
|
||||
object? Value,
|
||||
int EncodedLength);
|
||||
|
||||
public sealed record NmxSubscriptionRecord(
|
||||
int Status,
|
||||
int? DetailStatus,
|
||||
ushort Quality,
|
||||
DateTime TimestampUtc,
|
||||
byte WireKind,
|
||||
object? Value,
|
||||
int Offset,
|
||||
int Length)
|
||||
{
|
||||
public MxStatus ToDataChangeStatus()
|
||||
{
|
||||
return MxStatus.DataChangeOk;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record NmxSubscriptionMessage(
|
||||
byte Command,
|
||||
ushort Version,
|
||||
int RecordCount,
|
||||
Guid OperationId,
|
||||
Guid? ItemCorrelationId,
|
||||
IReadOnlyList<NmxSubscriptionRecord> Records)
|
||||
{
|
||||
public const byte SubscriptionStatusCommand = 0x32;
|
||||
public const byte DataUpdateCommand = 0x33;
|
||||
|
||||
public static NmxSubscriptionMessage ParseProcessDataReceivedBody(ReadOnlyMemory<byte> body)
|
||||
{
|
||||
var envelope = NmxObservedEnvelope.ParseProcessDataReceivedBodyFlexible(body);
|
||||
return ParseInner(envelope.InnerBody.Span);
|
||||
}
|
||||
|
||||
public static NmxSubscriptionMessage ParseInner(ReadOnlySpan<byte> inner)
|
||||
{
|
||||
if (inner.Length < 23)
|
||||
{
|
||||
throw new ArgumentException("NMX subscription callback body is too short.", nameof(inner));
|
||||
}
|
||||
|
||||
byte command = inner[0];
|
||||
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(inner.Slice(1, sizeof(ushort)));
|
||||
int recordCount = BinaryPrimitives.ReadInt32LittleEndian(inner.Slice(3, sizeof(int)));
|
||||
var operationId = new Guid(inner.Slice(7, 16));
|
||||
|
||||
return command switch
|
||||
{
|
||||
SubscriptionStatusCommand => ParseSubscriptionStatus(inner, version, recordCount, operationId),
|
||||
DataUpdateCommand => ParseDataUpdate(inner, version, recordCount, operationId),
|
||||
_ => throw new ArgumentException($"Unsupported NMX subscription callback command 0x{command:X2}.", nameof(inner)),
|
||||
};
|
||||
}
|
||||
|
||||
private static NmxSubscriptionMessage ParseDataUpdate(
|
||||
ReadOnlySpan<byte> inner,
|
||||
ushort version,
|
||||
int recordCount,
|
||||
Guid operationId)
|
||||
{
|
||||
if (recordCount != 1)
|
||||
{
|
||||
throw new ArgumentException("Observed NMX DataUpdate callback parser currently supports one record per body.", nameof(inner));
|
||||
}
|
||||
|
||||
const int recordOffset = 23;
|
||||
var record = ParseRecord(inner, recordOffset, hasDetailStatus: false);
|
||||
return new NmxSubscriptionMessage(
|
||||
DataUpdateCommand,
|
||||
version,
|
||||
recordCount,
|
||||
operationId,
|
||||
null,
|
||||
[record]);
|
||||
}
|
||||
|
||||
private static NmxSubscriptionMessage ParseSubscriptionStatus(
|
||||
ReadOnlySpan<byte> inner,
|
||||
ushort version,
|
||||
int recordCount,
|
||||
Guid operationId)
|
||||
{
|
||||
if (inner.Length < 39)
|
||||
{
|
||||
throw new ArgumentException("NMX SubscriptionStatus callback body is too short.", nameof(inner));
|
||||
}
|
||||
|
||||
var itemCorrelationId = new Guid(inner.Slice(23, 16));
|
||||
int offset = 39;
|
||||
List<NmxSubscriptionRecord> records = [];
|
||||
for (int i = 0; i < recordCount; i++)
|
||||
{
|
||||
var record = ParseRecord(inner, offset, hasDetailStatus: true);
|
||||
records.Add(record);
|
||||
offset += record.Length;
|
||||
}
|
||||
|
||||
return new NmxSubscriptionMessage(
|
||||
SubscriptionStatusCommand,
|
||||
version,
|
||||
recordCount,
|
||||
operationId,
|
||||
itemCorrelationId,
|
||||
records);
|
||||
}
|
||||
|
||||
private static NmxSubscriptionRecord ParseRecord(ReadOnlySpan<byte> body, int offset, bool hasDetailStatus)
|
||||
{
|
||||
int minimumLength = hasDetailStatus ? 19 : 15;
|
||||
if (offset < 0 || offset + minimumLength > body.Length)
|
||||
{
|
||||
throw new ArgumentException("NMX subscription record is too short.", nameof(body));
|
||||
}
|
||||
|
||||
int start = offset;
|
||||
int status = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
|
||||
offset += sizeof(int);
|
||||
|
||||
int? detailStatus = null;
|
||||
if (hasDetailStatus)
|
||||
{
|
||||
detailStatus = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
|
||||
offset += sizeof(int);
|
||||
}
|
||||
|
||||
ushort quality = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(offset, sizeof(ushort)));
|
||||
offset += sizeof(ushort);
|
||||
|
||||
long fileTime = BinaryPrimitives.ReadInt64LittleEndian(body.Slice(offset, sizeof(long)));
|
||||
offset += sizeof(long);
|
||||
|
||||
byte wireKind = body[offset++];
|
||||
var value = DecodeValue(wireKind, body[offset..]);
|
||||
offset += value.EncodedLength;
|
||||
|
||||
return new NmxSubscriptionRecord(
|
||||
status,
|
||||
detailStatus,
|
||||
quality,
|
||||
DateTime.FromFileTimeUtc(fileTime),
|
||||
wireKind,
|
||||
value.Value,
|
||||
start,
|
||||
offset - start);
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeValue(byte wireKind, ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length == 0)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, ToValueKindOrNull(wireKind), null, 0);
|
||||
}
|
||||
|
||||
return wireKind switch
|
||||
{
|
||||
0x01 when body.Length >= 1 => new NmxCallbackValue(wireKind, MxValueKind.Boolean, body[0] != 0, 1),
|
||||
0x02 when body.Length >= sizeof(int) => new NmxCallbackValue(wireKind, MxValueKind.Int32, BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)]), sizeof(int)),
|
||||
0x03 when body.Length >= sizeof(float) => new NmxCallbackValue(wireKind, MxValueKind.Float32, BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)])), sizeof(float)),
|
||||
0x04 when body.Length >= sizeof(double) => new NmxCallbackValue(wireKind, MxValueKind.Float64, BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(body[..sizeof(long)])), sizeof(double)),
|
||||
0x05 => DecodeStringValue(wireKind, body),
|
||||
0x06 => DecodeDateTimeValue(wireKind, body),
|
||||
0x07 => DecodeElapsedTimeValue(wireKind, body),
|
||||
0x41 or 0x42 or 0x43 or 0x44 or 0x45 or 0x46 => DecodeArrayValue(wireKind, body),
|
||||
_ => new NmxCallbackValue(wireKind, ToValueKindOrNull(wireKind), null, 0),
|
||||
};
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeStringValue(byte wireKind, ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < sizeof(int))
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.String, null, 0);
|
||||
}
|
||||
|
||||
int recordLength = BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)]);
|
||||
if (recordLength == sizeof(int))
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.String, string.Empty, sizeof(int));
|
||||
}
|
||||
|
||||
if (body.Length < 8)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.String, null, 0);
|
||||
}
|
||||
|
||||
int textByteLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(4, sizeof(int)));
|
||||
if (recordLength < 8 || textByteLength < 0 || recordLength != textByteLength + 4 || body.Length < 8 + textByteLength)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.String, null, 0);
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> textBytes = body.Slice(8, textByteLength);
|
||||
if (textBytes.Length >= 2 && textBytes[^2] == 0 && textBytes[^1] == 0)
|
||||
{
|
||||
textBytes = textBytes[..^2];
|
||||
}
|
||||
|
||||
string value = Encoding.Unicode.GetString(textBytes);
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.String, value, 8 + textByteLength);
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeDateTimeValue(byte wireKind, ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length >= 14)
|
||||
{
|
||||
int recordLength = BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)]);
|
||||
if (recordLength >= 10 && body.Length >= sizeof(int) + recordLength)
|
||||
{
|
||||
long fileTime = BinaryPrimitives.ReadInt64LittleEndian(body.Slice(sizeof(int), sizeof(long)));
|
||||
if (TryFromFileTimeUtc(fileTime, out DateTime timestamp))
|
||||
{
|
||||
return new NmxCallbackValue(
|
||||
wireKind,
|
||||
MxValueKind.DateTime,
|
||||
timestamp,
|
||||
sizeof(int) + recordLength);
|
||||
}
|
||||
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.DateTime, null, sizeof(int) + recordLength);
|
||||
}
|
||||
}
|
||||
|
||||
if (body.Length >= sizeof(long))
|
||||
{
|
||||
long fileTime = BinaryPrimitives.ReadInt64LittleEndian(body[..sizeof(long)]);
|
||||
if (TryFromFileTimeUtc(fileTime, out DateTime timestamp))
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.DateTime, timestamp, sizeof(long));
|
||||
}
|
||||
}
|
||||
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.DateTime, null, 0);
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeElapsedTimeValue(byte wireKind, ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < sizeof(int))
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.ElapsedTime, null, 0);
|
||||
}
|
||||
|
||||
int milliseconds = BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)]);
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.ElapsedTime, TimeSpan.FromMilliseconds(milliseconds), sizeof(int));
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeArrayValue(byte wireKind, ReadOnlySpan<byte> body)
|
||||
{
|
||||
const int arrayHeaderLength = 10;
|
||||
if (body.Length < arrayHeaderLength)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, ToValueKindOrNull(wireKind), null, 0);
|
||||
}
|
||||
|
||||
ushort count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(4, sizeof(ushort)));
|
||||
int elementWidth = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(6, sizeof(int)));
|
||||
ReadOnlySpan<byte> values = body[arrayHeaderLength..];
|
||||
|
||||
return wireKind switch
|
||||
{
|
||||
0x41 => DecodeBooleanArray(wireKind, count, elementWidth, body.Length, values),
|
||||
0x42 => DecodeInt32Array(wireKind, count, elementWidth, body.Length, values),
|
||||
0x43 => DecodeFloat32Array(wireKind, count, elementWidth, body.Length, values),
|
||||
0x44 => DecodeFloat64Array(wireKind, count, elementWidth, body.Length, values),
|
||||
0x45 => DecodeStringArray(wireKind, count, values),
|
||||
0x46 => DecodeDateTimeArray(wireKind, count, elementWidth, body.Length, values),
|
||||
_ => new NmxCallbackValue(wireKind, ToValueKindOrNull(wireKind), null, 0),
|
||||
};
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeBooleanArray(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
|
||||
{
|
||||
if (elementWidth != sizeof(short) || values.Length < count * elementWidth)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.BooleanArray, null, 0);
|
||||
}
|
||||
|
||||
bool[] decoded = new bool[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
decoded[i] = BinaryPrimitives.ReadInt16LittleEndian(values.Slice(i * elementWidth, sizeof(short))) != 0;
|
||||
}
|
||||
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.BooleanArray, decoded, 10 + count * elementWidth);
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeInt32Array(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
|
||||
{
|
||||
if (elementWidth != sizeof(int) || values.Length < count * elementWidth)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.Int32Array, null, 0);
|
||||
}
|
||||
|
||||
int[] decoded = new int[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
decoded[i] = BinaryPrimitives.ReadInt32LittleEndian(values.Slice(i * elementWidth, sizeof(int)));
|
||||
}
|
||||
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.Int32Array, decoded, 10 + count * elementWidth);
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeFloat32Array(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
|
||||
{
|
||||
if (elementWidth != sizeof(float) || values.Length < count * elementWidth)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.Float32Array, null, 0);
|
||||
}
|
||||
|
||||
float[] decoded = new float[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
decoded[i] = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(values.Slice(i * elementWidth, sizeof(int))));
|
||||
}
|
||||
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.Float32Array, decoded, 10 + count * elementWidth);
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeFloat64Array(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
|
||||
{
|
||||
if (elementWidth != sizeof(double) || values.Length < count * elementWidth)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.Float64Array, null, 0);
|
||||
}
|
||||
|
||||
double[] decoded = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
decoded[i] = BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(values.Slice(i * elementWidth, sizeof(long))));
|
||||
}
|
||||
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.Float64Array, decoded, 10 + count * elementWidth);
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeDateTimeArray(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
|
||||
{
|
||||
if (elementWidth != 12 || values.Length < count * elementWidth)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.DateTimeArray, null, 0);
|
||||
}
|
||||
|
||||
DateTime[] decoded = new DateTime[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
long fileTime = BinaryPrimitives.ReadInt64LittleEndian(values.Slice(i * elementWidth, sizeof(long)));
|
||||
decoded[i] = DateTime.FromFileTimeUtc(fileTime);
|
||||
}
|
||||
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.DateTimeArray, decoded, 10 + count * elementWidth);
|
||||
}
|
||||
|
||||
private static NmxCallbackValue DecodeStringArray(byte wireKind, ushort count, ReadOnlySpan<byte> values)
|
||||
{
|
||||
string[] decoded = new string[count];
|
||||
int offset = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (offset + 13 > values.Length)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.StringArray, null, 0);
|
||||
}
|
||||
|
||||
int recordLength = BinaryPrimitives.ReadInt32LittleEndian(values.Slice(offset, sizeof(int)));
|
||||
byte elementKind = values[offset + 4];
|
||||
int textRecordLength = BinaryPrimitives.ReadInt32LittleEndian(values.Slice(offset + 5, sizeof(int)));
|
||||
int textByteLength = BinaryPrimitives.ReadInt32LittleEndian(values.Slice(offset + 9, sizeof(int)));
|
||||
if (recordLength < 9 || elementKind != 0x05 || textRecordLength != textByteLength + sizeof(int) || recordLength != 1 + sizeof(int) + sizeof(int) + textByteLength || offset + 13 + textByteLength > values.Length)
|
||||
{
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.StringArray, null, 0);
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> textBytes = values.Slice(offset + 13, textByteLength);
|
||||
if (textBytes.Length >= 2 && textBytes[^2] == 0 && textBytes[^1] == 0)
|
||||
{
|
||||
textBytes = textBytes[..^2];
|
||||
}
|
||||
|
||||
decoded[i] = Encoding.Unicode.GetString(textBytes);
|
||||
offset += 13 + textByteLength;
|
||||
}
|
||||
|
||||
return new NmxCallbackValue(wireKind, MxValueKind.StringArray, decoded, 10 + offset);
|
||||
}
|
||||
|
||||
private static MxValueKind? ToValueKindOrNull(byte wireKind)
|
||||
{
|
||||
return wireKind switch
|
||||
{
|
||||
0x01 => MxValueKind.Boolean,
|
||||
0x02 => MxValueKind.Int32,
|
||||
0x03 => MxValueKind.Float32,
|
||||
0x04 => MxValueKind.Float64,
|
||||
0x05 => MxValueKind.String,
|
||||
0x06 => MxValueKind.DateTime,
|
||||
0x07 => MxValueKind.ElapsedTime,
|
||||
0x41 => MxValueKind.BooleanArray,
|
||||
0x42 => MxValueKind.Int32Array,
|
||||
0x43 => MxValueKind.Float32Array,
|
||||
0x44 => MxValueKind.Float64Array,
|
||||
0x45 => MxValueKind.StringArray,
|
||||
0x46 => MxValueKind.DateTimeArray,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryFromFileTimeUtc(long fileTime, out DateTime timestamp)
|
||||
{
|
||||
try
|
||||
{
|
||||
timestamp = DateTime.FromFileTimeUtc(fileTime);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
timestamp = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public enum NmxTransferMessageKind : int
|
||||
{
|
||||
Metadata = 1,
|
||||
ItemControl = 2,
|
||||
Write = 3,
|
||||
}
|
||||
|
||||
public sealed record NmxTransferEnvelope(
|
||||
NmxTransferMessageKind MessageKind,
|
||||
int SourceGalaxyId,
|
||||
int SourcePlatformId,
|
||||
int LocalEngineId,
|
||||
int TargetGalaxyId,
|
||||
int TargetPlatformId,
|
||||
int TargetEngineId,
|
||||
int TimeoutMilliseconds,
|
||||
ReadOnlyMemory<byte> InnerBody)
|
||||
{
|
||||
public const int HeaderLength = 46;
|
||||
private const ushort Version = 1;
|
||||
private const int InnerLengthOffset = 2;
|
||||
private const int ReservedOffset = 6;
|
||||
private const int MessageKindOffset = 10;
|
||||
private const int SourceGalaxyOffset = 14;
|
||||
private const int SourcePlatformOffset = 18;
|
||||
private const int LocalEngineOffset = 22;
|
||||
private const int TargetGalaxyOffset = 26;
|
||||
private const int TargetPlatformOffset = 30;
|
||||
private const int TargetEngineOffset = 34;
|
||||
private const int ProtocolMarkerOffset = 38;
|
||||
private const int TimeoutOffset = 42;
|
||||
private const int ProtocolMarker = 0x0201;
|
||||
private const int DefaultTimeoutMilliseconds = 30000;
|
||||
|
||||
public static NmxTransferEnvelope Parse(ReadOnlyMemory<byte> transferBody)
|
||||
{
|
||||
if (transferBody.Length < HeaderLength)
|
||||
{
|
||||
throw new ArgumentException("NMX TransferData body is too short.", nameof(transferBody));
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> span = transferBody.Span;
|
||||
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(span[..sizeof(ushort)]);
|
||||
if (version != Version)
|
||||
{
|
||||
throw new ArgumentException($"Unsupported NMX TransferData version {version}.", nameof(transferBody));
|
||||
}
|
||||
|
||||
int innerLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(InnerLengthOffset, sizeof(int)));
|
||||
if (innerLength != transferBody.Length - HeaderLength)
|
||||
{
|
||||
throw new ArgumentException("NMX TransferData inner length does not match the body size.", nameof(transferBody));
|
||||
}
|
||||
|
||||
int protocolMarker = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(ProtocolMarkerOffset, sizeof(int)));
|
||||
if (protocolMarker != ProtocolMarker)
|
||||
{
|
||||
throw new ArgumentException($"Unsupported NMX TransferData protocol marker 0x{protocolMarker:X8}.", nameof(transferBody));
|
||||
}
|
||||
|
||||
return new NmxTransferEnvelope(
|
||||
(NmxTransferMessageKind)BinaryPrimitives.ReadInt32LittleEndian(span.Slice(MessageKindOffset, sizeof(int))),
|
||||
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(SourceGalaxyOffset, sizeof(int))),
|
||||
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(SourcePlatformOffset, sizeof(int))),
|
||||
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(LocalEngineOffset, sizeof(int))),
|
||||
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(TargetGalaxyOffset, sizeof(int))),
|
||||
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(TargetPlatformOffset, sizeof(int))),
|
||||
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(TargetEngineOffset, sizeof(int))),
|
||||
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(TimeoutOffset, sizeof(int))),
|
||||
transferBody[HeaderLength..]);
|
||||
}
|
||||
|
||||
public static byte[] Encode(
|
||||
NmxTransferMessageKind messageKind,
|
||||
int localEngineId,
|
||||
int targetGalaxyId,
|
||||
int targetPlatformId,
|
||||
int targetEngineId,
|
||||
ReadOnlySpan<byte> innerBody,
|
||||
int sourceGalaxyId = 1,
|
||||
int sourcePlatformId = 1,
|
||||
int timeoutMilliseconds = DefaultTimeoutMilliseconds)
|
||||
{
|
||||
byte[] transferBody = new byte[HeaderLength + innerBody.Length];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(transferBody.AsSpan(0, sizeof(ushort)), Version);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(InnerLengthOffset, sizeof(int)), innerBody.Length);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(ReservedOffset, sizeof(int)), 0);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(MessageKindOffset, sizeof(int)), (int)messageKind);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(SourceGalaxyOffset, sizeof(int)), sourceGalaxyId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(SourcePlatformOffset, sizeof(int)), sourcePlatformId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(LocalEngineOffset, sizeof(int)), localEngineId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(TargetGalaxyOffset, sizeof(int)), targetGalaxyId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(TargetPlatformOffset, sizeof(int)), targetPlatformId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(TargetEngineOffset, sizeof(int)), targetEngineId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(ProtocolMarkerOffset, sizeof(int)), ProtocolMarker);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(TimeoutOffset, sizeof(int)), timeoutMilliseconds);
|
||||
innerBody.CopyTo(transferBody.AsSpan(HeaderLength));
|
||||
return transferBody;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public sealed class NmxTransferEnvelopeTemplate
|
||||
{
|
||||
public const int HeaderLength = 46;
|
||||
public const int InnerLengthOffset = 2;
|
||||
|
||||
private readonly byte[] _header;
|
||||
|
||||
private NmxTransferEnvelopeTemplate(byte[] header)
|
||||
{
|
||||
_header = header;
|
||||
}
|
||||
|
||||
public static NmxTransferEnvelopeTemplate FromObserved(ReadOnlySpan<byte> observedTransferBody)
|
||||
{
|
||||
if (observedTransferBody.Length < HeaderLength)
|
||||
{
|
||||
throw new ArgumentException("Observed TransferData body is too short.", nameof(observedTransferBody));
|
||||
}
|
||||
|
||||
int innerLength = BinaryPrimitives.ReadInt32LittleEndian(observedTransferBody.Slice(InnerLengthOffset, sizeof(int)));
|
||||
if (innerLength != observedTransferBody.Length - HeaderLength)
|
||||
{
|
||||
throw new ArgumentException("Observed TransferData body does not contain the expected inner length.", nameof(observedTransferBody));
|
||||
}
|
||||
|
||||
return new NmxTransferEnvelopeTemplate(observedTransferBody[..HeaderLength].ToArray());
|
||||
}
|
||||
|
||||
public byte[] Encode(ReadOnlySpan<byte> innerPutRequestBody)
|
||||
{
|
||||
byte[] body = new byte[HeaderLength + innerPutRequestBody.Length];
|
||||
_header.CopyTo(body, 0);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(InnerLengthOffset, sizeof(int)), innerPutRequestBody.Length);
|
||||
innerPutRequestBody.CopyTo(body.AsSpan(HeaderLength));
|
||||
return body;
|
||||
}
|
||||
|
||||
public ReadOnlyMemory<byte> DecodeInner(ReadOnlyMemory<byte> transferBody)
|
||||
{
|
||||
if (transferBody.Length < HeaderLength)
|
||||
{
|
||||
throw new ArgumentException("TransferData body is too short.", nameof(transferBody));
|
||||
}
|
||||
|
||||
int innerLength = BinaryPrimitives.ReadInt32LittleEndian(transferBody.Span.Slice(InnerLengthOffset, sizeof(int)));
|
||||
if (innerLength != transferBody.Length - HeaderLength)
|
||||
{
|
||||
throw new ArgumentException("TransferData body inner length does not match the body size.", nameof(transferBody));
|
||||
}
|
||||
|
||||
return transferBody[HeaderLength..];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public static class NmxWriteMessage
|
||||
{
|
||||
public const byte Command = 0x37;
|
||||
public const ushort Version = 1;
|
||||
public const int HandleProjectionOffset = 3;
|
||||
public const int HandleProjectionLength = 14;
|
||||
public const int KindOffset = 17;
|
||||
|
||||
public static byte[] Encode(
|
||||
MxReferenceHandle referenceHandle,
|
||||
MxValueKind valueKind,
|
||||
object value,
|
||||
int writeIndex = 1,
|
||||
uint clientToken = 0)
|
||||
{
|
||||
byte[] valueBytes = EncodeValue(valueKind, value);
|
||||
byte[] body = valueKind switch
|
||||
{
|
||||
MxValueKind.Boolean => CreateBoolean(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
|
||||
MxValueKind.Int32 or MxValueKind.Float32 => CreateFixed(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
|
||||
MxValueKind.Float64 => CreateFixed(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
|
||||
MxValueKind.String or MxValueKind.DateTime => CreateVariable(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
|
||||
MxValueKind.BooleanArray or MxValueKind.Int32Array or MxValueKind.Float32Array or MxValueKind.Float64Array => CreateArray(referenceHandle, valueKind, valueBytes, GetArrayCount(value), GetArrayElementWidth(valueKind), writeIndex, clientToken),
|
||||
MxValueKind.StringArray or MxValueKind.DateTimeArray => CreateArray(referenceHandle, valueKind, valueBytes, GetArrayCount(value), elementWidth: 4, writeIndex, clientToken),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
||||
};
|
||||
return body;
|
||||
}
|
||||
|
||||
public static byte[] EncodeTimestamped(
|
||||
MxReferenceHandle referenceHandle,
|
||||
MxValueKind valueKind,
|
||||
object value,
|
||||
DateTime timestamp,
|
||||
int writeIndex = 1,
|
||||
uint clientToken = 0)
|
||||
{
|
||||
byte[] valueBytes = EncodeValue(valueKind, value);
|
||||
byte[] body = valueKind switch
|
||||
{
|
||||
MxValueKind.Boolean => CreateBooleanTimestamped(referenceHandle, value, timestamp, writeIndex, clientToken),
|
||||
MxValueKind.Int32 or MxValueKind.Float32 => CreateFixedTimestamped(referenceHandle, valueKind, valueBytes, timestamp, writeIndex, clientToken),
|
||||
MxValueKind.Float64 => CreateFixedTimestamped(referenceHandle, valueKind, valueBytes, timestamp, writeIndex, clientToken),
|
||||
MxValueKind.String or MxValueKind.DateTime => CreateVariableTimestamped(referenceHandle, valueKind, valueBytes, timestamp, writeIndex, clientToken),
|
||||
MxValueKind.BooleanArray or MxValueKind.Int32Array or MxValueKind.Float32Array or MxValueKind.Float64Array => CreateArrayTimestamped(referenceHandle, valueKind, valueBytes, GetArrayCount(value), GetArrayElementWidth(valueKind), timestamp, writeIndex, clientToken),
|
||||
MxValueKind.StringArray or MxValueKind.DateTimeArray => CreateArrayTimestamped(referenceHandle, valueKind, valueBytes, GetArrayCount(value), elementWidth: 4, timestamp, writeIndex, clientToken),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
||||
};
|
||||
return body;
|
||||
}
|
||||
|
||||
public static MxValueKind GetValueKind(short mxDataType, bool isArray)
|
||||
{
|
||||
if (TryGetValueKind(mxDataType, isArray, out MxValueKind valueKind))
|
||||
{
|
||||
return valueKind;
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(mxDataType), $"Unsupported MX data type {mxDataType} with isArray={isArray}.");
|
||||
}
|
||||
|
||||
public static bool TryGetValueKind(short mxDataType, bool isArray, out MxValueKind valueKind)
|
||||
{
|
||||
return (mxDataType, isArray) switch
|
||||
{
|
||||
(1, false) => Return(MxValueKind.Boolean, out valueKind),
|
||||
(2, false) => Return(MxValueKind.Int32, out valueKind),
|
||||
(3, false) => Return(MxValueKind.Float32, out valueKind),
|
||||
(4, false) => Return(MxValueKind.Float64, out valueKind),
|
||||
(5, false) => Return(MxValueKind.String, out valueKind),
|
||||
(6, false) => Return(MxValueKind.DateTime, out valueKind),
|
||||
(1, true) => Return(MxValueKind.BooleanArray, out valueKind),
|
||||
(2, true) => Return(MxValueKind.Int32Array, out valueKind),
|
||||
(3, true) => Return(MxValueKind.Float32Array, out valueKind),
|
||||
(4, true) => Return(MxValueKind.Float64Array, out valueKind),
|
||||
(5, true) => Return(MxValueKind.StringArray, out valueKind),
|
||||
(6, true) => Return(MxValueKind.DateTimeArray, out valueKind),
|
||||
_ => Return(default, out valueKind, success: false),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool Return(MxValueKind source, out MxValueKind target, bool success = true)
|
||||
{
|
||||
target = source;
|
||||
return success;
|
||||
}
|
||||
|
||||
public static byte GetWireKind(MxValueKind valueKind)
|
||||
{
|
||||
return valueKind switch
|
||||
{
|
||||
MxValueKind.Boolean => 0x01,
|
||||
MxValueKind.Int32 => 0x02,
|
||||
MxValueKind.Float32 => 0x03,
|
||||
MxValueKind.Float64 => 0x04,
|
||||
MxValueKind.String or MxValueKind.DateTime => 0x05,
|
||||
MxValueKind.BooleanArray => 0x41,
|
||||
MxValueKind.Int32Array => 0x42,
|
||||
MxValueKind.Float32Array => 0x43,
|
||||
MxValueKind.Float64Array => 0x44,
|
||||
MxValueKind.StringArray or MxValueKind.DateTimeArray => 0x45,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] CreateFixed(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, int writeIndex, uint clientToken)
|
||||
{
|
||||
byte[] body = new byte[KindOffset + 1 + valueBytes.Length + 14 + sizeof(int)];
|
||||
WriteCommonPrefix(body, referenceHandle, valueKind);
|
||||
valueBytes.CopyTo(body.AsSpan(KindOffset + 1));
|
||||
WriteNormalSuffix(body.AsSpan(KindOffset + 1 + valueBytes.Length), writeIndex, clientToken);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] CreateBoolean(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, int writeIndex, uint clientToken)
|
||||
{
|
||||
byte[] body = new byte[KindOffset + 1 + valueBytes.Length + 11 + sizeof(int)];
|
||||
WriteCommonPrefix(body, referenceHandle, valueKind);
|
||||
valueBytes.CopyTo(body.AsSpan(KindOffset + 1));
|
||||
WriteBooleanSuffix(body.AsSpan(KindOffset + 1 + valueBytes.Length), writeIndex, clientToken);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] CreateBooleanTimestamped(MxReferenceHandle referenceHandle, object value, DateTime timestamp, int writeIndex, uint clientToken)
|
||||
{
|
||||
byte[] body = new byte[KindOffset + 1 + sizeof(byte) + 14 + sizeof(int)];
|
||||
WriteCommonPrefix(body, referenceHandle, MxValueKind.Boolean);
|
||||
body[KindOffset + 1] = Convert.ToBoolean(value, CultureInfo.InvariantCulture) ? (byte)0xff : (byte)0x00;
|
||||
WriteTimestampedSuffix(body.AsSpan(KindOffset + 2), timestamp, writeIndex, clientToken);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] CreateFixedTimestamped(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, DateTime timestamp, int writeIndex, uint clientToken)
|
||||
{
|
||||
byte[] body = new byte[KindOffset + 1 + valueBytes.Length + 14 + sizeof(int)];
|
||||
WriteCommonPrefix(body, referenceHandle, valueKind);
|
||||
valueBytes.CopyTo(body.AsSpan(KindOffset + 1));
|
||||
WriteTimestampedSuffix(body.AsSpan(KindOffset + 1 + valueBytes.Length), timestamp, writeIndex, clientToken);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] CreateVariable(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, int writeIndex, uint clientToken)
|
||||
{
|
||||
byte[] body = new byte[KindOffset + 1 + sizeof(int) + sizeof(int) + valueBytes.Length + 14 + sizeof(int)];
|
||||
WriteCommonPrefix(body, referenceHandle, valueKind);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(18, sizeof(int)), valueBytes.Length + sizeof(int));
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(22, sizeof(int)), valueBytes.Length);
|
||||
valueBytes.CopyTo(body.AsSpan(26));
|
||||
WriteNormalSuffix(body.AsSpan(26 + valueBytes.Length), writeIndex, clientToken);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] CreateVariableTimestamped(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, DateTime timestamp, int writeIndex, uint clientToken)
|
||||
{
|
||||
byte[] body = new byte[KindOffset + 1 + sizeof(int) + sizeof(int) + valueBytes.Length + 14 + sizeof(int)];
|
||||
WriteCommonPrefix(body, referenceHandle, valueKind);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(18, sizeof(int)), valueBytes.Length + sizeof(int));
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(22, sizeof(int)), valueBytes.Length);
|
||||
valueBytes.CopyTo(body.AsSpan(26));
|
||||
WriteTimestampedSuffix(body.AsSpan(26 + valueBytes.Length), timestamp, writeIndex, clientToken);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] CreateArray(
|
||||
MxReferenceHandle referenceHandle,
|
||||
MxValueKind valueKind,
|
||||
byte[] valueBytes,
|
||||
int count,
|
||||
ushort elementWidth,
|
||||
int writeIndex,
|
||||
uint clientToken)
|
||||
{
|
||||
byte[] body = new byte[KindOffset + 1 + 10 + valueBytes.Length + 14 + sizeof(int)];
|
||||
WriteCommonPrefix(body, referenceHandle, valueKind);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22, sizeof(ushort)), checked((ushort)count));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(24, sizeof(ushort)), elementWidth);
|
||||
valueBytes.CopyTo(body.AsSpan(28));
|
||||
WriteNormalSuffix(body.AsSpan(28 + valueBytes.Length), writeIndex, clientToken);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] CreateArrayTimestamped(
|
||||
MxReferenceHandle referenceHandle,
|
||||
MxValueKind valueKind,
|
||||
byte[] valueBytes,
|
||||
int count,
|
||||
ushort elementWidth,
|
||||
DateTime timestamp,
|
||||
int writeIndex,
|
||||
uint clientToken)
|
||||
{
|
||||
byte[] body = new byte[KindOffset + 1 + 10 + valueBytes.Length + 14 + sizeof(int)];
|
||||
WriteCommonPrefix(body, referenceHandle, valueKind);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22, sizeof(ushort)), checked((ushort)count));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(24, sizeof(ushort)), elementWidth);
|
||||
valueBytes.CopyTo(body.AsSpan(28));
|
||||
WriteTimestampedSuffix(body.AsSpan(28 + valueBytes.Length), timestamp, writeIndex, clientToken);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static void WriteCommonPrefix(byte[] body, MxReferenceHandle referenceHandle, MxValueKind valueKind)
|
||||
{
|
||||
body[0] = Command;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(1, sizeof(ushort)), Version);
|
||||
referenceHandle.Encode().AsSpan(6, HandleProjectionLength).CopyTo(body.AsSpan(HandleProjectionOffset, HandleProjectionLength));
|
||||
body[KindOffset] = GetWireKind(valueKind);
|
||||
}
|
||||
|
||||
private static void WriteNormalSuffix(Span<byte> suffixAndIndex, int writeIndex, uint clientToken)
|
||||
{
|
||||
if (suffixAndIndex.Length != 18)
|
||||
{
|
||||
throw new ArgumentException("Normal write suffix span must include the 14-byte suffix and 4-byte write index.", nameof(suffixAndIndex));
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt16LittleEndian(suffixAndIndex[..sizeof(short)], -1);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(suffixAndIndex.Slice(2, sizeof(ulong)), 0);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(suffixAndIndex.Slice(10, sizeof(uint)), clientToken);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(suffixAndIndex.Slice(14, sizeof(int)), writeIndex);
|
||||
}
|
||||
|
||||
private static void WriteBooleanSuffix(Span<byte> suffixAndIndex, int writeIndex, uint clientToken)
|
||||
{
|
||||
if (suffixAndIndex.Length != 15)
|
||||
{
|
||||
throw new ArgumentException("Boolean write suffix span must include the 11-byte suffix and 4-byte write index.", nameof(suffixAndIndex));
|
||||
}
|
||||
|
||||
suffixAndIndex[..7].Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(suffixAndIndex.Slice(7, sizeof(uint)), clientToken);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(suffixAndIndex.Slice(11, sizeof(int)), writeIndex);
|
||||
}
|
||||
|
||||
private static void WriteTimestampedSuffix(Span<byte> suffixAndIndex, DateTime timestamp, int writeIndex, uint clientToken)
|
||||
{
|
||||
if (suffixAndIndex.Length != 18)
|
||||
{
|
||||
throw new ArgumentException("Timestamped write suffix span must include the 14-byte suffix and 4-byte write index.", nameof(suffixAndIndex));
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt16LittleEndian(suffixAndIndex[..sizeof(short)], 0);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(suffixAndIndex.Slice(2, sizeof(long)), timestamp.ToFileTime());
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(suffixAndIndex.Slice(10, sizeof(uint)), clientToken);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(suffixAndIndex.Slice(14, sizeof(int)), writeIndex);
|
||||
}
|
||||
|
||||
private static byte[] EncodeValue(MxValueKind valueKind, object value)
|
||||
{
|
||||
return valueKind switch
|
||||
{
|
||||
MxValueKind.Boolean => Convert.ToBoolean(value, CultureInfo.InvariantCulture) ? [0xff, 0xff, 0xff, 0x00] : [0x00, 0xff, 0xff, 0x00],
|
||||
MxValueKind.Int32 => EncodeInt32(Convert.ToInt32(value, CultureInfo.InvariantCulture)),
|
||||
MxValueKind.Float32 => EncodeFloat32(Convert.ToSingle(value, CultureInfo.InvariantCulture)),
|
||||
MxValueKind.Float64 => EncodeFloat64(Convert.ToDouble(value, CultureInfo.InvariantCulture)),
|
||||
MxValueKind.String => EncodeUtf16String(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty),
|
||||
MxValueKind.DateTime => EncodeUtf16String(FormatDateTime((DateTime)value)),
|
||||
MxValueKind.BooleanArray => EncodeBooleanArray((bool[])value),
|
||||
MxValueKind.Int32Array => EncodeInt32Array((int[])value),
|
||||
MxValueKind.Float32Array => EncodeFloat32Array((float[])value),
|
||||
MxValueKind.Float64Array => EncodeFloat64Array((double[])value),
|
||||
MxValueKind.StringArray => EncodeVariableArray(((string[])value).Select(static value => value ?? string.Empty)),
|
||||
MxValueKind.DateTimeArray => EncodeVariableArray(((DateTime[])value).Select(FormatDateTime)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] EncodeInt32(int value)
|
||||
{
|
||||
byte[] bytes = new byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeFloat32(float value)
|
||||
{
|
||||
byte[] bytes = new byte[sizeof(float)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, BitConverter.SingleToInt32Bits(value));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeFloat64(double value)
|
||||
{
|
||||
byte[] bytes = new byte[sizeof(double)];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(bytes, BitConverter.DoubleToInt64Bits(value));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeUtf16String(string value)
|
||||
{
|
||||
byte[] textBytes = Encoding.Unicode.GetBytes(value);
|
||||
byte[] bytes = new byte[textBytes.Length + 2];
|
||||
textBytes.CopyTo(bytes, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeBooleanArray(bool[] values)
|
||||
{
|
||||
byte[] bytes = new byte[values.Length * sizeof(short)];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt16LittleEndian(bytes.AsSpan(i * sizeof(short), sizeof(short)), values[i] ? (short)-1 : (short)0);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeInt32Array(int[] values)
|
||||
{
|
||||
byte[] bytes = new byte[values.Length * sizeof(int)];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(int), sizeof(int)), values[i]);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeFloat32Array(float[] values)
|
||||
{
|
||||
byte[] bytes = new byte[values.Length * sizeof(float)];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(float), sizeof(float)), BitConverter.SingleToInt32Bits(values[i]));
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeFloat64Array(double[] values)
|
||||
{
|
||||
byte[] bytes = new byte[values.Length * sizeof(double)];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt64LittleEndian(bytes.AsSpan(i * sizeof(double), sizeof(double)), BitConverter.DoubleToInt64Bits(values[i]));
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeVariableArray(IEnumerable<string> values)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
foreach (string value in values)
|
||||
{
|
||||
byte[] textBytes = EncodeUtf16String(value);
|
||||
byte[] header = new byte[13];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(0, 4), 1 + sizeof(int) + sizeof(int) + textBytes.Length);
|
||||
header[4] = 0x05;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(5, 4), textBytes.Length + sizeof(int));
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(9, 4), textBytes.Length);
|
||||
stream.Write(header);
|
||||
stream.Write(textBytes);
|
||||
}
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static int GetArrayCount(object value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
bool[] values => values.Length,
|
||||
int[] values => values.Length,
|
||||
float[] values => values.Length,
|
||||
double[] values => values.Length,
|
||||
string[] values => values.Length,
|
||||
DateTime[] values => values.Length,
|
||||
_ => throw new ArgumentException("Value is not a supported MX array.", nameof(value)),
|
||||
};
|
||||
}
|
||||
|
||||
private static ushort GetArrayElementWidth(MxValueKind valueKind)
|
||||
{
|
||||
return valueKind switch
|
||||
{
|
||||
MxValueKind.BooleanArray => sizeof(short),
|
||||
MxValueKind.Int32Array => sizeof(int),
|
||||
MxValueKind.Float32Array => sizeof(float),
|
||||
MxValueKind.Float64Array => sizeof(double),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatDateTime(DateTime value)
|
||||
{
|
||||
return value.ToString("M/d/yyyy h:mm:ss tt", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace MxNativeCodec;
|
||||
|
||||
public sealed class ObservedWriteBodyTemplate
|
||||
{
|
||||
public const int FixedValueOffset = 18;
|
||||
public const int VariableValueOffset = 26;
|
||||
public const int ArrayValueOffset = 28;
|
||||
|
||||
private readonly byte[] _prefix;
|
||||
private readonly byte[] _suffixBeforeWriteIndex;
|
||||
private readonly MxValueKind _kind;
|
||||
|
||||
private ObservedWriteBodyTemplate(MxValueKind kind, byte[] prefix, byte[] suffixBeforeWriteIndex)
|
||||
{
|
||||
_kind = kind;
|
||||
_prefix = prefix;
|
||||
_suffixBeforeWriteIndex = suffixBeforeWriteIndex;
|
||||
}
|
||||
|
||||
public MxValueKind Kind => _kind;
|
||||
|
||||
public static ObservedWriteBodyTemplate FromObserved(MxValueKind kind, ReadOnlySpan<byte> observedBody)
|
||||
{
|
||||
if (observedBody.Length < 24)
|
||||
{
|
||||
throw new ArgumentException("Observed body is too short.", nameof(observedBody));
|
||||
}
|
||||
|
||||
return kind switch
|
||||
{
|
||||
MxValueKind.Boolean => CreateFixed(kind, observedBody, valueWidth: 4),
|
||||
MxValueKind.Int32 => CreateFixed(kind, observedBody, valueWidth: 4),
|
||||
MxValueKind.Float32 => CreateFixed(kind, observedBody, valueWidth: 4),
|
||||
MxValueKind.Float64 => CreateFixed(kind, observedBody, valueWidth: 8),
|
||||
MxValueKind.String => CreateVariable(kind, observedBody),
|
||||
MxValueKind.DateTime => CreateVariable(kind, observedBody),
|
||||
MxValueKind.BooleanArray => CreateArray(kind, observedBody),
|
||||
MxValueKind.Int32Array => CreateArray(kind, observedBody),
|
||||
MxValueKind.Float32Array => CreateArray(kind, observedBody),
|
||||
MxValueKind.Float64Array => CreateArray(kind, observedBody),
|
||||
MxValueKind.StringArray => CreateArray(kind, observedBody),
|
||||
MxValueKind.DateTimeArray => CreateArray(kind, observedBody),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
||||
};
|
||||
}
|
||||
|
||||
public byte[] Encode(object value, int writeIndex)
|
||||
{
|
||||
byte[] valueBytes = EncodeValue(value);
|
||||
byte[] body = new byte[_prefix.Length + valueBytes.Length + _suffixBeforeWriteIndex.Length + sizeof(int)];
|
||||
|
||||
_prefix.CopyTo(body, 0);
|
||||
valueBytes.CopyTo(body, _prefix.Length);
|
||||
_suffixBeforeWriteIndex.CopyTo(body, _prefix.Length + valueBytes.Length);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(body.Length - sizeof(int)), writeIndex);
|
||||
PatchVariableLengths(body, valueBytes.Length);
|
||||
PatchArrayDescriptor(body, valueBytes.Length);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
public object Decode(ReadOnlySpan<byte> body)
|
||||
{
|
||||
return _kind switch
|
||||
{
|
||||
MxValueKind.Boolean => DecodeBoolean(body),
|
||||
MxValueKind.Int32 => BinaryPrimitives.ReadInt32LittleEndian(body.Slice(FixedValueOffset, 4)),
|
||||
MxValueKind.Float32 => BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(FixedValueOffset, 4))),
|
||||
MxValueKind.Float64 => BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(body.Slice(FixedValueOffset, 8))),
|
||||
MxValueKind.String => DecodeUtf16String(body),
|
||||
MxValueKind.DateTime => DecodeDateTime(body),
|
||||
MxValueKind.BooleanArray => DecodeBooleanArray(body),
|
||||
MxValueKind.Int32Array => DecodeInt32Array(body),
|
||||
MxValueKind.Float32Array => DecodeFloat32Array(body),
|
||||
MxValueKind.Float64Array => DecodeFloat64Array(body),
|
||||
MxValueKind.StringArray => DecodeVariableArray(body, DecodeVariableArrayString),
|
||||
MxValueKind.DateTimeArray => DecodeVariableArray(body, DecodeVariableArrayDateTime),
|
||||
_ => throw new InvalidOperationException($"Unsupported value kind {_kind}."),
|
||||
};
|
||||
}
|
||||
|
||||
public int DecodeWriteIndex(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < sizeof(int))
|
||||
{
|
||||
throw new ArgumentException("Body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
return BinaryPrimitives.ReadInt32LittleEndian(body[^sizeof(int)..]);
|
||||
}
|
||||
|
||||
private static ObservedWriteBodyTemplate CreateFixed(MxValueKind kind, ReadOnlySpan<byte> body, int valueWidth)
|
||||
{
|
||||
int suffixStart = FixedValueOffset + valueWidth;
|
||||
int suffixLength = body.Length - suffixStart - sizeof(int);
|
||||
if (suffixLength < 0)
|
||||
{
|
||||
throw new ArgumentException("Observed fixed-width body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
return new ObservedWriteBodyTemplate(
|
||||
kind,
|
||||
body[..FixedValueOffset].ToArray(),
|
||||
body.Slice(suffixStart, suffixLength).ToArray());
|
||||
}
|
||||
|
||||
private static ObservedWriteBodyTemplate CreateVariable(MxValueKind kind, ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < VariableValueOffset + sizeof(int))
|
||||
{
|
||||
throw new ArgumentException("Observed variable-width body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
int valueByteLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(22, 4));
|
||||
int suffixStart = VariableValueOffset + valueByteLength;
|
||||
int suffixLength = body.Length - suffixStart - sizeof(int);
|
||||
if (valueByteLength < 2 || suffixLength < 0)
|
||||
{
|
||||
throw new ArgumentException("Observed variable-width body has invalid lengths.", nameof(body));
|
||||
}
|
||||
|
||||
return new ObservedWriteBodyTemplate(
|
||||
kind,
|
||||
body[..VariableValueOffset].ToArray(),
|
||||
body.Slice(suffixStart, suffixLength).ToArray());
|
||||
}
|
||||
|
||||
private static ObservedWriteBodyTemplate CreateArray(MxValueKind kind, ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < ArrayValueOffset + 18)
|
||||
{
|
||||
throw new ArgumentException("Observed array body is too short.", nameof(body));
|
||||
}
|
||||
|
||||
int suffixStart = body.Length - 18;
|
||||
return new ObservedWriteBodyTemplate(
|
||||
kind,
|
||||
body[..ArrayValueOffset].ToArray(),
|
||||
body.Slice(suffixStart, 14).ToArray());
|
||||
}
|
||||
|
||||
private byte[] EncodeValue(object value)
|
||||
{
|
||||
return _kind switch
|
||||
{
|
||||
MxValueKind.Boolean => EncodeBoolean(Convert.ToBoolean(value, CultureInfo.InvariantCulture)),
|
||||
MxValueKind.Int32 => EncodeInt32(Convert.ToInt32(value, CultureInfo.InvariantCulture)),
|
||||
MxValueKind.Float32 => EncodeFloat32(Convert.ToSingle(value, CultureInfo.InvariantCulture)),
|
||||
MxValueKind.Float64 => EncodeFloat64(Convert.ToDouble(value, CultureInfo.InvariantCulture)),
|
||||
MxValueKind.String => EncodeUtf16String(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty),
|
||||
MxValueKind.DateTime => EncodeUtf16String(FormatObservedDateTime((DateTime)value)),
|
||||
MxValueKind.BooleanArray => EncodeBooleanArray((bool[])value),
|
||||
MxValueKind.Int32Array => EncodeInt32Array((int[])value),
|
||||
MxValueKind.Float32Array => EncodeFloat32Array((float[])value),
|
||||
MxValueKind.Float64Array => EncodeFloat64Array((double[])value),
|
||||
MxValueKind.StringArray => EncodeVariableArray(((string[])value).Select(static s => s ?? string.Empty)),
|
||||
MxValueKind.DateTimeArray => EncodeVariableArray(((DateTime[])value).Select(FormatObservedDateTime)),
|
||||
_ => throw new InvalidOperationException($"Unsupported value kind {_kind}."),
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] EncodeBoolean(bool value)
|
||||
{
|
||||
return value
|
||||
? [0xff, 0xff, 0xff, 0x00]
|
||||
: [0x00, 0xff, 0xff, 0x00];
|
||||
}
|
||||
|
||||
private static bool DecodeBoolean(ReadOnlySpan<byte> body)
|
||||
{
|
||||
return body[FixedValueOffset] == 0xff && body[FixedValueOffset + 1] == 0xff;
|
||||
}
|
||||
|
||||
private static byte[] EncodeInt32(int value)
|
||||
{
|
||||
byte[] bytes = new byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeBooleanArray(bool[] values)
|
||||
{
|
||||
byte[] bytes = new byte[values.Length * sizeof(short)];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt16LittleEndian(bytes.AsSpan(i * sizeof(short), sizeof(short)), values[i] ? (short)-1 : (short)0);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static bool[] DecodeBooleanArray(ReadOnlySpan<byte> body)
|
||||
{
|
||||
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
|
||||
bool[] values = new bool[count];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = BinaryPrimitives.ReadInt16LittleEndian(body.Slice(ArrayValueOffset + i * sizeof(short), sizeof(short))) != 0;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static byte[] EncodeInt32Array(int[] values)
|
||||
{
|
||||
byte[] bytes = new byte[values.Length * sizeof(int)];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(int), sizeof(int)), values[i]);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static int[] DecodeInt32Array(ReadOnlySpan<byte> body)
|
||||
{
|
||||
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
|
||||
int[] values = new int[count];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(ArrayValueOffset + i * sizeof(int), sizeof(int)));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static byte[] EncodeFloat32(float value)
|
||||
{
|
||||
byte[] bytes = new byte[sizeof(float)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, BitConverter.SingleToInt32Bits(value));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeFloat32Array(float[] values)
|
||||
{
|
||||
byte[] bytes = new byte[values.Length * sizeof(float)];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(float), sizeof(float)), BitConverter.SingleToInt32Bits(values[i]));
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static float[] DecodeFloat32Array(ReadOnlySpan<byte> body)
|
||||
{
|
||||
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
|
||||
float[] values = new float[count];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(ArrayValueOffset + i * sizeof(float), sizeof(float))));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static byte[] EncodeFloat64(double value)
|
||||
{
|
||||
byte[] bytes = new byte[sizeof(double)];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(bytes, BitConverter.DoubleToInt64Bits(value));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] EncodeFloat64Array(double[] values)
|
||||
{
|
||||
byte[] bytes = new byte[values.Length * sizeof(double)];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt64LittleEndian(bytes.AsSpan(i * sizeof(double), sizeof(double)), BitConverter.DoubleToInt64Bits(values[i]));
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static double[] DecodeFloat64Array(ReadOnlySpan<byte> body)
|
||||
{
|
||||
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
|
||||
double[] values = new double[count];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(body.Slice(ArrayValueOffset + i * sizeof(double), sizeof(double))));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static byte[] EncodeUtf16String(string value)
|
||||
{
|
||||
byte[] textBytes = Encoding.Unicode.GetBytes(value);
|
||||
byte[] bytes = new byte[textBytes.Length + 2];
|
||||
textBytes.CopyTo(bytes, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static string DecodeUtf16String(ReadOnlySpan<byte> body)
|
||||
{
|
||||
int valueByteLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(22, 4));
|
||||
ReadOnlySpan<byte> raw = body.Slice(VariableValueOffset, valueByteLength);
|
||||
if (raw.Length >= 2 && raw[^1] == 0 && raw[^2] == 0)
|
||||
{
|
||||
raw = raw[..^2];
|
||||
}
|
||||
|
||||
return Encoding.Unicode.GetString(raw);
|
||||
}
|
||||
|
||||
private static DateTime DecodeDateTime(ReadOnlySpan<byte> body)
|
||||
{
|
||||
string text = DecodeUtf16String(body);
|
||||
return DateTime.ParseExact(text, "M/d/yyyy h:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None);
|
||||
}
|
||||
|
||||
private static byte[] EncodeVariableArray(IEnumerable<string> values)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
foreach (var value in values)
|
||||
{
|
||||
byte[] textBytes = EncodeUtf16String(value);
|
||||
byte[] header = new byte[13];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(0, 4), 1 + sizeof(int) + sizeof(int) + textBytes.Length);
|
||||
header[4] = 0x05;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(5, 4), textBytes.Length + sizeof(int));
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(9, 4), textBytes.Length);
|
||||
stream.Write(header);
|
||||
stream.Write(textBytes, 0, textBytes.Length);
|
||||
}
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static T[] DecodeVariableArray<T>(ReadOnlySpan<byte> body, Func<ReadOnlySpan<byte>, T> decode)
|
||||
{
|
||||
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
|
||||
T[] values = new T[count];
|
||||
int offset = ArrayValueOffset;
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
int recordLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, 4));
|
||||
ReadOnlySpan<byte> record = body.Slice(offset + 4, recordLength);
|
||||
values[i] = decode(record);
|
||||
offset += sizeof(int) + recordLength;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static string DecodeVariableArrayString(ReadOnlySpan<byte> record)
|
||||
{
|
||||
if (record[0] != 0x05)
|
||||
{
|
||||
throw new ArgumentException("Unexpected variable array element type.", nameof(record));
|
||||
}
|
||||
|
||||
int valueByteLength = BinaryPrimitives.ReadInt32LittleEndian(record.Slice(5, 4));
|
||||
ReadOnlySpan<byte> raw = record.Slice(9, valueByteLength);
|
||||
if (raw.Length >= 2 && raw[^1] == 0 && raw[^2] == 0)
|
||||
{
|
||||
raw = raw[..^2];
|
||||
}
|
||||
|
||||
return Encoding.Unicode.GetString(raw);
|
||||
}
|
||||
|
||||
private static DateTime DecodeVariableArrayDateTime(ReadOnlySpan<byte> record)
|
||||
{
|
||||
return DateTime.ParseExact(DecodeVariableArrayString(record), "M/d/yyyy h:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None);
|
||||
}
|
||||
|
||||
private static string FormatObservedDateTime(DateTime value)
|
||||
{
|
||||
return value.ToString("M/d/yyyy h:mm:ss tt", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void PatchVariableLengths(byte[] body, int valueByteLength)
|
||||
{
|
||||
if (_kind is not (MxValueKind.String or MxValueKind.DateTime))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(18, 4), valueByteLength + 4);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(22, 4), valueByteLength);
|
||||
return;
|
||||
}
|
||||
|
||||
private void PatchArrayDescriptor(byte[] body, int valueByteLength)
|
||||
{
|
||||
if (_kind is not (MxValueKind.BooleanArray or MxValueKind.Int32Array or MxValueKind.Float32Array or MxValueKind.Float64Array or MxValueKind.StringArray or MxValueKind.DateTimeArray))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_kind is MxValueKind.StringArray or MxValueKind.DateTimeArray)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int count = _kind switch
|
||||
{
|
||||
MxValueKind.BooleanArray => valueByteLength / sizeof(short),
|
||||
MxValueKind.Int32Array => valueByteLength / sizeof(int),
|
||||
MxValueKind.Float32Array => valueByteLength / sizeof(float),
|
||||
MxValueKind.Float64Array => valueByteLength / sizeof(double),
|
||||
_ => 0,
|
||||
};
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22, 2), checked((ushort)count));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user