Files
mxaccess/src/MxNativeCodec/NmxWriteMessage.cs
T
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
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>
2026-05-05 06:21:00 -04:00

395 lines
18 KiB
C#

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