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 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 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 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 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); } }