using System.Buffers.Binary; using System.Text; namespace MxNativeCodec; public sealed record NmxObservedEnvelope( bool HasLengthPrefix, int? TotalLengthPrefix, int DeclaredInnerLength, int ActualInnerLength, ReadOnlyMemory Header, ReadOnlyMemory InnerBody) { public const int HeaderLength = 46; public const int InnerLengthOffset = 2; public static NmxObservedEnvelope ParseTransferDataBody(ReadOnlyMemory 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 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 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 Strings) { public static NmxObservedMessage Parse(ReadOnlySpan 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 ExtractUtf16Strings(ReadOnlySpan body) { List 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; } }