Initial project state: .NET reference, design, Rust port (M0+M1), evidence
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:
Joseph Doherty
2026-05-05 06:21:00 -04:00
parent 43733699b0
commit fe2a6db786
3849 changed files with 352975 additions and 0 deletions
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MxNativeClient\MxNativeClient.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
</Project>
+658
View File
@@ -0,0 +1,658 @@
using MxNativeClient;
using MxNativeCodec;
RunObjRefParseTest();
RunOrpcStructureTests();
RunObjectExporterMessageTests();
RunNmxService2MessageTests();
RunNmxSvcCallbackMessageTests();
RunDceRpcAuthTrailerTests();
RunRemUnknownMessageTests();
RunDceRpcBindParseTest();
RunDceRpcRequestParseTest();
RunDceRpcResponseParseTest();
await RunGalaxyRepositoryResolverTestAsync();
await RunGalaxyRepositoryUserResolverTestAsync();
RunManagedNmxService2ClientTransferBodyTests();
RunCompatibilitySurfaceTests();
RunRecoveryPolicyTests();
RunOperationStatusTests();
Console.WriteLine("MxNativeClient protocol primitive tests passed.");
static void RunObjRefParseTest()
{
// Captured from: dotnet run MxNativeClient.Probe -- --dump-objref --objref-context=2
byte[] prefix = FromHex(
"4d 45 4f 57 01 00 00 00 00 00 00 00 00 00 00 00 c0 00 00 00 00 00 00 46 " +
"00 00 00 00 01 00 00 00 c2 5b ab 3b b5 d2 f0 ea 53 46 0b 86 8b 5f 57 39 " +
"20 f8 00 00 78 36 00 00 33 3c 4c 10 66 e0 ac 8b 95 00 7f 00");
var objRef = ComObjRef.Parse(prefix);
AssertEqual("OBJREF signature", 0x574F454Du, objRef.Signature);
AssertEqual("OBJREF flags", 1u, objRef.Flags);
AssertEqual("OBJREF iid", new Guid("00000000-0000-0000-C000-000000000046"), objRef.Iid);
AssertEqual("OBJREF OXID", 0xEAF0D2B53BAB5BC2ul, objRef.Oxid);
AssertEqual("OBJREF OID", 0x39575F8B860B4653ul, objRef.Oid);
AssertEqual("OBJREF entries", (ushort)149, objRef.DualStringEntries);
var std = StdObjRef.Parse(prefix.AsSpan(24, StdObjRef.EncodedLength));
AssertEqual("STD OXID", objRef.Oxid, std.Oxid);
AssertEqual("STD OID", objRef.Oid, std.Oid);
AssertEqual("STD IPID", objRef.Ipid, std.Ipid);
}
static void RunCompatibilitySurfaceTests()
{
Type serverType = typeof(MxNativeCompatibilityServer);
AssertNotNull(
"compat Write2 object timestamp overload",
serverType.GetMethod(
nameof(MxNativeCompatibilityServer.Write2),
[
typeof(int),
typeof(int),
typeof(object),
typeof(object),
typeof(int),
]));
AssertNotNull(
"compat WriteSecured2 object timestamp overload",
serverType.GetMethod(
nameof(MxNativeCompatibilityServer.WriteSecured2),
[
typeof(int),
typeof(int),
typeof(int),
typeof(int),
typeof(object),
typeof(object),
]));
AssertNotNull(
"compat recover method",
serverType.GetMethod(nameof(MxNativeCompatibilityServer.RecoverConnection), [typeof(int)]));
AssertNotNull(
"compat async recover method",
serverType.GetMethod(
nameof(MxNativeCompatibilityServer.RecoverConnectionAsync),
[typeof(int), typeof(MxNativeRecoveryPolicy), typeof(CancellationToken)]));
AssertNotNull("compat data-change event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.DataChanged)));
AssertNotNull("compat buffered data-change event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.BufferedDataChanged)));
AssertNotNull("compat write-complete event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.WriteCompleted)));
AssertNotNull("compat operation-complete event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.OperationCompleted)));
AssertNotNull("compat recovery-attempt event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.RecoveryAttemptStarted)));
AssertNotNull("compat recovery-failed event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.RecoveryAttemptFailed)));
AssertNotNull("compat recovery-completed event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.RecoveryCompleted)));
AssertNotNull(
"session recover method",
typeof(MxNativeSession).GetMethod(nameof(MxNativeSession.RecoverConnection), Type.EmptyTypes));
AssertNotNull(
"session async recover method",
typeof(MxNativeSession).GetMethod(
nameof(MxNativeSession.RecoverConnectionAsync),
[typeof(MxNativeRecoveryPolicy), typeof(CancellationToken)]));
AssertNotNull("session recovery-attempt event", typeof(MxNativeSession).GetEvent(nameof(MxNativeSession.RecoveryAttemptStarted)));
AssertNotNull("session recovery-failed event", typeof(MxNativeSession).GetEvent(nameof(MxNativeSession.RecoveryAttemptFailed)));
AssertNotNull("session recovery-completed event", typeof(MxNativeSession).GetEvent(nameof(MxNativeSession.RecoveryCompleted)));
AssertNotNull(
"callback recovery marker",
typeof(MxNativeCallbackEvent).GetProperty(nameof(MxNativeCallbackEvent.IsDuringRecovery)));
AssertNotNull(
"operation status recovery marker",
typeof(MxNativeOperationStatusEvent).GetProperty(nameof(MxNativeOperationStatusEvent.IsDuringRecovery)));
AssertNotNull(
"reference registration recovery marker",
typeof(MxNativeReferenceRegistrationEvent).GetProperty(nameof(MxNativeReferenceRegistrationEvent.IsDuringRecovery)));
AssertNotNull(
"unparsed callback recovery marker",
typeof(MxNativeUnparsedCallbackEvent).GetProperty(nameof(MxNativeUnparsedCallbackEvent.IsDuringRecovery)));
AssertNotNull(
"compat data-change recovery marker",
typeof(MxNativeDataChangeEvent).GetProperty(nameof(MxNativeDataChangeEvent.IsDuringRecovery)));
AssertNotNull(
"compat write-complete recovery marker",
typeof(MxNativeWriteCompleteEvent).GetProperty(nameof(MxNativeWriteCompleteEvent.IsDuringRecovery)));
AssertNotNull(
"compat buffered data-change recovery marker",
typeof(MxNativeBufferedDataChangeEvent).GetProperty(nameof(MxNativeBufferedDataChangeEvent.IsDuringRecovery)));
AssertNotNull(
"managed nmx heartbeat method",
typeof(ManagedNmxService2Client).GetMethod(
nameof(ManagedNmxService2Client.SetHeartbeatSendInterval),
[typeof(int), typeof(int)]));
}
static void RunRecoveryPolicyTests()
{
new MxNativeRecoveryPolicy { MaxAttempts = 1, Delay = TimeSpan.Zero }.Validate();
AssertThrows<ArgumentOutOfRangeException>(
"recovery attempts validation",
() => new MxNativeRecoveryPolicy { MaxAttempts = 0 }.Validate());
AssertThrows<ArgumentOutOfRangeException>(
"recovery delay validation",
() => new MxNativeRecoveryPolicy { Delay = TimeSpan.FromMilliseconds(-1) }.Validate());
}
static void RunOperationStatusTests()
{
AssertEqual("completion-only 00 parses", true, NmxOperationStatusMessage.TryParseInner([0x00], out var completionOk));
AssertEqual("completion-only format", NmxOperationStatusFormat.CompletionOnly, completionOk.Format);
AssertEqual("completion-only 00 code", (byte)0x00, completionOk.CompletionCode);
AssertEqual("completion-only not mxaccess write complete", false, completionOk.IsMxAccessWriteComplete);
AssertEqual("completion-only 00 success", (short)0, completionOk.Status.Success);
AssertEqual("completion-only 00 category", MxStatusCategory.Unknown, completionOk.Status.Category);
AssertEqual("completion-only 00 detail", (short)0, completionOk.Status.Detail);
AssertEqual("completion-only 41 parses", true, NmxOperationStatusMessage.TryParseInner([0x41], out var completionBadType));
AssertEqual("completion-only 41 detail", (short)0x41, completionBadType.Status.Detail);
AssertEqual("completion-only 41 category", MxStatusCategory.Unknown, completionBadType.Status.Category);
AssertEqual("completion-only 41 not mxaccess write complete", false, completionBadType.IsMxAccessWriteComplete);
AssertEqual("completion-only ef parses", true, NmxOperationStatusMessage.TryParseInner([0xef], out var completionInternationalized));
AssertEqual("completion-only ef detail", (short)0xef, completionInternationalized.Status.Detail);
AssertEqual("completion-only ef not mxaccess write complete", false, completionInternationalized.IsMxAccessWriteComplete);
AssertEqual("status-word write complete parses", true, NmxOperationStatusMessage.TryParseInner([0x00, 0x00, 0x50, 0x80, 0x00], out var statusWord));
AssertEqual("status-word format", NmxOperationStatusFormat.StatusWord, statusWord.Format);
AssertEqual("status-word code", (ushort)0x8050, statusWord.StatusCode);
AssertEqual("status-word mxaccess write complete", true, statusWord.IsMxAccessWriteComplete);
AssertEqual("status-word status", MxStatus.WriteCompleteOk, statusWord.Status);
}
static void AssertThrows<TException>(string name, Action action)
where TException : Exception
{
try
{
action();
}
catch (TException)
{
return;
}
throw new InvalidOperationException($"{name}: expected {typeof(TException).Name}.");
}
static void AssertNotNull(string name, object? actual)
{
if (actual is null)
{
throw new InvalidOperationException($"{name}: expected non-null value.");
}
}
static void RunOrpcStructureTests()
{
var cid = new Guid("01234567-89AB-CDEF-0123-456789ABCDEF");
var thisCall = OrpcThis.Create(cid);
byte[] encodedThis = thisCall.Encode();
AssertEqual("ORPCTHIS length", OrpcThis.EncodedLengthWithoutExtensions, encodedThis.Length);
AssertEqual("ORPCTHIS version", ComVersion.Version57, OrpcThis.Parse(encodedThis).Version);
AssertEqual("ORPCTHIS cid", cid, OrpcThis.Parse(encodedThis).Cid);
var thatCall = new OrpcThat(0, 0);
byte[] encodedThat = thatCall.Encode();
AssertEqual("ORPCTHAT length", OrpcThat.EncodedLengthWithoutExtensions, encodedThat.Length);
AssertEqual("ORPCTHAT flags", 0u, OrpcThat.Parse(encodedThat).Flags);
byte[] objRef = FromHex(
"4d 45 4f 57 01 00 00 00 00 00 00 00 00 00 00 00 c0 00 00 00 00 00 00 46 " +
"00 00 00 00 01 00 00 00 c2 5b ab 3b b5 d2 f0 ea 53 46 0b 86 8b 5f 57 39 " +
"20 f8 00 00 78 36 00 00 33 3c 4c 10 66 e0 ac 8b 95 00 7f 00");
var mip = new MInterfacePointer(objRef);
AssertBytes("MInterfacePointer payload", objRef, MInterfacePointer.Parse(mip.Encode()).ObjRefBytes);
}
static void RunRemUnknownMessageTests()
{
var ipid = new Guid("00007c14-3678-0000-fa76-a0a5cd73ac85");
var iid = NmxProcedureMetadata.INmxService2;
var cid = new Guid("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
byte[] request = RemUnknownMessages.EncodeRemQueryInterfaceRequest(ipid, iid, cid, publicRefs: 5);
AssertEqual("RemQI length", 76, request.Length);
AssertEqual("RemQI ORPCTHIS CID", cid, OrpcThis.Parse(request).Cid);
AssertEqual("RemQI source IPID", ipid, new Guid(request.AsSpan(OrpcThis.EncodedLengthWithoutExtensions, 16)));
AssertEqual("RemQI refs", 5u, ReadUInt32(request, 48));
AssertEqual("RemQI iid count", (ushort)1, ReadUInt16(request, 52));
AssertEqual("RemQI conformant max", 1u, ReadUInt32(request, 56));
AssertEqual("RemQI requested IID", iid, new Guid(request.AsSpan(60, 16)));
var std = new StdObjRef(0, 5, 0x0102030405060708, 0x1112131415161718, ipid);
byte[] resultBytes = new byte[RemQiResult.EncodedLength];
std.Encode().CopyTo(resultBytes.AsSpan(8));
var result = RemQiResult.Parse(resultBytes);
AssertEqual("RemQI result hr", 0, result.HResult);
AssertEqual("RemQI result ipid", ipid, result.StandardObjectReference.Ipid);
}
static void RunObjectExporterMessageTests()
{
byte[] expected = FromHex("c2 5b ab 3b b5 d2 f0 ea 01 00 00 00 01 00 00 00 07 00 00 00");
byte[] actual = ObjectExporterMessages.EncodeResolveOxidRequest(
0xEAF0D2B53BAB5BC2,
[ObjectExporterMessages.ProtseqNcacnIpTcp]);
AssertBytes("ResolveOxid request", expected, actual);
var failure = ObjectExporterMessages.ParseResolveOxidFailure(FromHex(
"00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 05 00 00 00"));
AssertEqual("ResolveOxid unauth status", 5u, failure.ErrorStatus);
}
static void RunNmxService2MessageTests()
{
var cid = new Guid("11111111-2222-3333-4444-555555555555");
var orpcThis = OrpcThis.Create(cid);
byte[] getVersion = NmxService2Messages.EncodeGetPartnerVersionRequest(orpcThis, 1, 2, 0x7ffd);
AssertEqual("GetPartnerVersion opnum", (ushort)11, NmxService2Messages.GetPartnerVersionOpnum);
AssertEqual("GetPartnerVersion request length", 44, getVersion.Length);
AssertEqual("GetPartnerVersion cid", cid, OrpcThis.Parse(getVersion).Cid);
AssertEqual("GetPartnerVersion galaxy", 1u, ReadUInt32(getVersion, 32));
AssertEqual("GetPartnerVersion platform", 2u, ReadUInt32(getVersion, 36));
AssertEqual("GetPartnerVersion engine", 0x7ffdu, ReadUInt32(getVersion, 40));
byte[] response = FromHex("00 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00");
var parsed = NmxService2Messages.ParseGetPartnerVersionResponse(response);
AssertEqual("GetPartnerVersion response version", 6, parsed.PartnerVersion);
AssertEqual("GetPartnerVersion response hr", 0, parsed.HResult);
byte[] connect = NmxService2Messages.EncodeConnectRequest(orpcThis, 0x7fee, 1, 1, 0x7ffd);
AssertEqual("Connect request length", 48, connect.Length);
AssertEqual("Connect local engine", 0x7feeu, ReadUInt32(connect, 32));
byte[] subscriber = NmxService2Messages.EncodeSubscriberEngineRequest(orpcThis, 0x7fee, 1, 1, 0x7ffd);
AssertEqual("Subscriber request length", 48, subscriber.Length);
AssertEqual("Subscriber local engine", 0x7feeu, ReadUInt32(subscriber, 32));
AssertEqual("Subscriber remote engine", 0x7ffdu, ReadUInt32(subscriber, 44));
byte[] unregister = NmxService2Messages.EncodeUnregisterEngineRequest(orpcThis, 0x7fee);
AssertEqual("UnRegisterEngine request length", 36, unregister.Length);
AssertEqual("UnRegisterEngine local engine", 0x7feeu, ReadUInt32(unregister, 32));
byte[] heartbeat = NmxService2Messages.EncodeSetHeartbeatSendIntervalRequest(orpcThis, 5, 3);
AssertEqual("Heartbeat request length", 40, heartbeat.Length);
AssertEqual("Heartbeat ticks", 5u, ReadUInt32(heartbeat, 32));
AssertEqual("Heartbeat misses", 3u, ReadUInt32(heartbeat, 36));
byte[] transferBody = FromHex(
"01 00 00 00 00 00 00 00 00 00 9e 91 04 00 01 00 00 00 " +
"01 00 00 00 fb 7f 00 00 01 00 00 00 01 00 00 00 02 00 " +
"00 00 02 02 00 00 30 75 00 00");
byte[] transfer = NmxService2Messages.EncodeTransferDataRequest(orpcThis, 1, 1, 2, transferBody);
AssertEqual("TransferData opnum", (ushort)6, NmxService2Messages.TransferDataOpnum);
AssertEqual("TransferData request length", 100, transfer.Length);
AssertEqual("TransferData galaxy", 1u, ReadUInt32(transfer, 32));
AssertEqual("TransferData platform", 1u, ReadUInt32(transfer, 36));
AssertEqual("TransferData engine", 2u, ReadUInt32(transfer, 40));
AssertEqual("TransferData lSize", 46u, ReadUInt32(transfer, 44));
AssertEqual("TransferData max count", 46u, ReadUInt32(transfer, 48));
AssertBytes("TransferData body", transferBody, transfer.AsSpan(52, transferBody.Length));
var hresult = NmxService2Messages.ParseHResultResponse(FromHex("00 00 00 00 00 00 00 00 00 00 00 00"));
AssertEqual("HRESULT response", 0, hresult.HResult);
byte[] bstr = NmxService2Messages.EncodeBstrUserMarshal("NmxComProxyWire5");
AssertBytes(
"BSTR user marshal",
FromHex(
"10 00 00 00 20 00 00 00 10 00 00 00 4e 00 6d 00 " +
"78 00 43 00 6f 00 6d 00 50 00 72 00 6f 00 78 00 " +
"79 00 57 00 69 00 72 00 65 00 35 00"),
bstr);
byte[] register = NmxService2Messages.EncodeRegisterEngine2Request(orpcThis, 0x7104, "NmxComProxyWire5", 6);
AssertEqual("RegisterEngine2 opnum", (ushort)10, NmxService2Messages.RegisterEngine2Opnum);
AssertEqual("RegisterEngine2 null callback length", 92, register.Length);
AssertEqual("RegisterEngine2 engine", 0x7104u, ReadUInt32(register, 32));
AssertEqual("RegisterEngine2 BSTR marker", 0x72657355u, ReadUInt32(register, 36));
AssertBytes("RegisterEngine2 BSTR", bstr, register.AsSpan(40, bstr.Length));
AssertEqual("RegisterEngine2 version", 6u, ReadUInt32(register, 40 + bstr.Length));
AssertEqual("RegisterEngine2 null callback", 0u, ReadUInt32(register, 44 + bstr.Length));
byte[] oddRegister = NmxService2Messages.EncodeRegisterEngine2Request(orpcThis, 0x7200, "MxManagedNullCallback", 6);
AssertEqual("RegisterEngine2 odd-name length", 104, oddRegister.Length);
AssertEqual("RegisterEngine2 odd-name marker", 0x72657355u, ReadUInt32(oddRegister, 36));
AssertEqual("RegisterEngine2 odd-name version", 6u, ReadUInt32(oddRegister, 96));
AssertEqual("RegisterEngine2 odd-name null callback", 0u, ReadUInt32(oddRegister, 100));
byte[] compactObjRef = new byte[68];
byte[] callbackRegister = NmxService2Messages.EncodeRegisterEngine2Request(orpcThis, 0x7108, "NmxCallbackX86", 6, compactObjRef);
AssertEqual("RegisterEngine2 callback marker", 0x00020000u, ReadUInt32(callbackRegister, 84));
AssertEqual("RegisterEngine2 callback size a", 68u, ReadUInt32(callbackRegister, 88));
AssertEqual("RegisterEngine2 callback size b", 68u, ReadUInt32(callbackRegister, 92));
}
static void RunNmxSvcCallbackMessageTests()
{
var cid = new Guid("22222222-3333-4444-5555-666666666666");
byte[] body = FromHex("01 02 03 04 05");
byte[] request = new byte[OrpcThis.EncodedLengthWithoutExtensions + 8 + body.Length + 3];
OrpcThis.Create(cid).Encode().CopyTo(request.AsSpan());
WriteUInt32(request, 32, (uint)body.Length);
WriteUInt32(request, 36, (uint)body.Length);
body.CopyTo(request.AsSpan(40));
var parsed = NmxSvcCallbackMessages.ParseCallbackRequest(request);
AssertEqual("callback iid", NmxProcedureMetadata.INmxSvcCallback, NmxSvcCallbackMessages.InterfaceId);
AssertEqual("callback data opnum", (ushort)3, NmxSvcCallbackMessages.DataReceivedOpnum);
AssertEqual("callback status opnum", (ushort)4, NmxSvcCallbackMessages.StatusReceivedOpnum);
AssertEqual("callback cid", cid, parsed.OrpcThis.Cid);
AssertBytes("callback body", body, parsed.Body);
byte[] response = NmxSvcCallbackMessages.EncodeCallbackResponse(0);
AssertEqual("callback response length", 12, response.Length);
AssertEqual("callback response hr", 0u, ReadUInt32(response, 8));
}
static void RunDceRpcAuthTrailerTests()
{
var trailer = new DceRpcAuthTrailer(DceRpcAuthType.WinNt, DceRpcAuthLevel.Connect, 0, 0, 79231);
byte[] token = [0x4e, 0x54, 0x4c, 0x4d];
var bind = new DceRpcBindPdu(
new DceRpcPduHeader(5, 0, DceRpcPacketType.Bind, 0x03, 0x10, 0, 0, 1),
4280,
4280,
0,
[
new DceRpcPresentationContext(
0,
new DceRpcSyntaxId(ObjectExporterMessages.IObjectExporter, 0, 0),
[DceRpcSyntaxId.Ndr20]),
]);
byte[] pdu = bind.EncodeWithAuth(trailer, token);
var header = DceRpcPduHeader.Parse(pdu);
AssertEqual("auth bind token len", (ushort)4, header.AuthLength);
var auth = DceRpcBindPdu.ReadAuthValue(pdu);
AssertEqual("auth trailer type", DceRpcAuthType.WinNt, auth.Trailer.AuthType);
AssertEqual("auth trailer level", DceRpcAuthLevel.Connect, auth.Trailer.AuthLevel);
AssertBytes("auth token", token, auth.Token.Span);
}
static void RunDceRpcBindParseTest()
{
byte[] bind = FromHex(
"05 00 0b 03 10 00 00 00 74 00 00 00 02 00 00 00 d0 16 d0 16 00 00 00 00 " +
"02 00 00 00 00 00 01 00 df 90 0c 4e 9d e3 64 41 a4 21 ac e8 94 84 c6 02 " +
"01 00 00 00 04 5d 88 8a eb 1c c9 11 9f e8 08 00 2b 10 48 60 02 00 00 00 " +
"01 00 01 00 df 90 0c 4e 9d e3 64 41 a4 21 ac e8 94 84 c6 02 01 00 00 00 " +
"2c 1c b7 6c 12 98 40 45 03 00 00 00 00 00 00 00 01 00 00 00");
var pdu = DceRpcBindPdu.Parse(bind);
AssertEqual("bind type", DceRpcPacketType.Bind, pdu.Header.PacketType);
AssertEqual("bind frag", (ushort)116, pdu.Header.FragmentLength);
AssertEqual("bind call", 2u, pdu.Header.CallId);
AssertEqual("bind contexts", 2, pdu.PresentationContexts.Count);
AssertEqual("bind context0", (ushort)0, pdu.PresentationContexts[0].ContextId);
AssertEqual("bind context0 syntax", new Guid("4E0C90DF-E39D-4164-A421-ACE89484C602"), pdu.PresentationContexts[0].AbstractSyntax.Uuid);
byte[] encoded = pdu.Encode();
AssertBytes("bind roundtrip", bind, encoded);
}
static void RunDceRpcRequestParseTest()
{
byte[] request = FromHex(
"05 00 00 03 10 00 00 00 28 00 00 00 02 00 00 00 10 00 00 00 00 00 00 00 " +
"8c 9e 28 85 62 f2 97 47 84 df 3e 4b a6 0e 98 7f");
var pdu = DceRpcRequestPdu.Parse(request);
AssertEqual("request type", DceRpcPacketType.Request, pdu.Header.PacketType);
AssertEqual("request call", 2u, pdu.Header.CallId);
AssertEqual("request context", (ushort)0, pdu.ContextId);
AssertEqual("request opnum", (ushort)0, pdu.Opnum);
AssertEqual("request stub length", 16, pdu.StubData.Length);
}
static void RunDceRpcResponseParseTest()
{
byte[] response = FromHex(
"05 00 02 03 10 00 00 00 2c 00 00 00 02 00 00 00 14 00 00 00 00 00 00 00 " +
"00 00 00 00 e5 96 74 5a a4 eb c2 4f b6 13 bf 8a c5 a8 3b 6e");
var pdu = DceRpcResponsePdu.Parse(response);
AssertEqual("response type", DceRpcPacketType.Response, pdu.Header.PacketType);
AssertEqual("response call", 2u, pdu.Header.CallId);
AssertEqual("response context", (ushort)0, pdu.ContextId);
AssertEqual("response stub length", 20, pdu.StubData.Length);
}
static async Task RunGalaxyRepositoryResolverTestAsync()
{
var resolver = new GalaxyRepositoryTagResolver();
GalaxyTagMetadata tag = await resolver.ResolveAsync("TestChildObject.TestInt");
AssertEqual("GR object", "TestChildObject", tag.ObjectTagName);
AssertEqual("GR attribute", "TestInt", tag.AttributeName);
AssertEqual("GR primitive", (string?)null, tag.PrimitiveName);
AssertEqual("GR platform", (ushort)1, tag.PlatformId);
AssertEqual("GR engine", (ushort)2, tag.EngineId);
AssertEqual("GR object id", (ushort)5, tag.ObjectId);
AssertEqual("GR primitive id", (short)2, tag.PrimitiveId);
AssertEqual("GR attribute id", (short)155, tag.AttributeId);
AssertEqual("GR property id", (short)10, tag.PropertyId);
AssertEqual("GR data type", (short)2, tag.MxDataType);
AssertEqual("GR is array", false, tag.IsArray);
AssertEqual("GR source", "dynamic", tag.AttributeSource);
AssertEqual("GR value kind", MxValueKind.Int32, tag.ToValueKind());
AssertEqual("GR supported value kind", true, tag.IsSupportedValueKind);
AssertEqual("GR try value kind", true, tag.TryGetValueKind(out var testIntKind));
AssertEqual("GR try value kind result", MxValueKind.Int32, testIntKind);
byte[] expected = FromHex("01 00 01 00 02 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00");
AssertBytes("GR synthesized handle", expected, tag.ToReferenceHandle().Encode());
GalaxyTagMetadata bufferedProperty = await resolver.ResolveAsync("TestChildObject.TestInt.property(buffer)");
AssertEqual("GR property(buffer) base attribute", "TestInt", bufferedProperty.AttributeName);
AssertEqual("GR property(buffer) id", GalaxyTagMetadata.BufferPropertyId, bufferedProperty.PropertyId);
AssertEqual("GR property(buffer) marker", true, bufferedProperty.IsBufferProperty);
AssertBytes(
"GR property(buffer) native literal handle",
FromHex("01 00 01 00 02 00 05 00 36 d7 02 00 9b 00 32 00 3e da 00 00"),
bufferedProperty.ToReferenceHandle().Encode());
GalaxyTagMetadata shortDesc = await resolver.ResolveAsync("TestChildObject.ShortDesc");
AssertEqual("GR ShortDesc category-independent property", (short)10, shortDesc.PropertyId);
AssertBytes(
"GR ShortDesc native value handle",
FromHex("01 00 01 00 02 00 05 00 36 d7 02 00 65 00 0a 00 6d 02 00 00"),
shortDesc.ToReferenceHandle().Encode());
GalaxyTagMetadata secured = await resolver.ResolveAsync("TestMachine_001.ProtectedValue");
AssertEqual("GR secured object", "TestMachine_001", secured.ObjectTagName);
AssertEqual("GR secured attribute", "ProtectedValue", secured.AttributeName);
AssertEqual("GR secured object id", (ushort)6, secured.ObjectId);
AssertEqual("GR secured attr id", (short)166, secured.AttributeId);
AssertEqual("GR secured class", (short)2, secured.SecurityClassification);
AssertEqual("GR secured kind", MxValueKind.Boolean, secured.ToValueKind());
AssertBytes(
"GR secured handle",
FromHex("01 00 01 00 02 00 06 00 08 f4 02 00 a6 00 0a 00 bb 67 00 00"),
secured.ToReferenceHandle().Encode());
GalaxyTagMetadata verified = await resolver.ResolveAsync("TestMachine_001.ProtectedValue1");
AssertEqual("GR verified attr id", (short)167, verified.AttributeId);
AssertEqual("GR verified class", (short)3, verified.SecurityClassification);
AssertBytes(
"GR verified handle",
FromHex("01 00 01 00 02 00 06 00 08 f4 02 00 a7 00 0a 00 26 8a 00 00"),
verified.ToReferenceHandle().Encode());
GalaxyTagMetadata elapsed = await resolver.ResolveAsync("TestMachine_001.TestAlarm001.Alarm.TimeDeadband");
AssertEqual("GR dotted primitive object", "TestMachine_001", elapsed.ObjectTagName);
AssertEqual("GR dotted primitive", "TestAlarm001", elapsed.PrimitiveName);
AssertEqual("GR dotted primitive attribute", "Alarm.TimeDeadband", elapsed.AttributeName);
AssertEqual("GR dotted primitive data type", (short)MxDataType.ElapsedTime, elapsed.MxDataType);
AssertEqual("GR dotted primitive supported", false, elapsed.IsSupportedValueKind);
var elapsedProjection = elapsed.ProjectWriteValue(TimeSpan.FromSeconds(1));
AssertEqual("GR elapsed projection kind", MxValueKind.Int32, elapsedProjection.ValueKind);
AssertEqual("GR elapsed projection value", 1000, elapsedProjection.Value);
GalaxyTagMetadata internationalized = await resolver.ResolveAsync("DevAppEngine.Scheduler._EngUnitsMB");
AssertEqual("GR internationalized primitive", "Scheduler", internationalized.PrimitiveName);
AssertEqual("GR internationalized attribute", "_EngUnitsMB", internationalized.AttributeName);
AssertEqual("GR internationalized data type", (short)MxDataType.InternationalizedString, internationalized.MxDataType);
AssertEqual("GR internationalized supported", false, internationalized.IsSupportedValueKind);
var internationalizedProjection = internationalized.ProjectWriteValue("MB");
AssertEqual("GR internationalized projection kind", MxValueKind.String, internationalizedProjection.ValueKind);
AssertEqual("GR internationalized projection value", "MB", internationalizedProjection.Value);
IReadOnlyList<GalaxyTagMetadata> browsed = await resolver.BrowseAsync("TestChildObject", "Test%", maxRows: 25);
AssertEqual("GR browse includes TestInt", true, browsed.Any(item => item.ObjectTagName == "TestChildObject" && item.AttributeName == "TestInt"));
AssertEqual("GR browse max respected", true, browsed.Count <= 25);
}
static async Task RunGalaxyRepositoryUserResolverTestAsync()
{
var resolver = new GalaxyRepositoryUserResolver();
GalaxyUserProfile administrator = await resolver.ResolveByNameAsync("Administrator");
AssertEqual("GR user name", "Administrator", administrator.UserProfileName);
AssertEqual("GR user id", 2, administrator.UserProfileId);
AssertEqual("GR user guid", new Guid("9222FBBA-53F4-457E-8B37-C93A9A250B4A"), administrator.UserGuid);
AssertEqual("GR user group", "Default", administrator.DefaultSecurityGroup);
AssertEqual("GR user role administrator", true, administrator.Roles.Contains("Administrator"));
AssertEqual("GR user role default", true, administrator.Roles.Contains("Default"));
AssertEqual(
"GR user guid to profile id",
administrator.UserProfileId,
await resolver.ResolveUserProfileIdByGuidAsync(administrator.UserGuid));
}
static void RunManagedNmxService2ClientTransferBodyTests()
{
var tag = new GalaxyTagMetadata(
ObjectTagName: "TestChildObject",
AttributeName: "ShortDesc",
PrimitiveName: null,
PlatformId: 1,
EngineId: 2,
ObjectId: 5,
PrimitiveId: 2,
AttributeId: 101,
PropertyId: 10,
MxDataType: (short)MxDataType.InternationalizedString,
IsArray: false,
SecurityClassification: 1,
AttributeSource: "dynamic");
byte[] writeTransfer = ManagedNmxService2Client.EncodeWriteTransferBody(
localEngineId: 0x7ffa,
tag,
"hello-native",
writeIndex: 1,
clientToken: 0x0bff6894);
var writeEnvelope = NmxTransferEnvelope.Parse(writeTransfer);
AssertEqual("client write transfer kind", NmxTransferMessageKind.Write, writeEnvelope.MessageKind);
AssertEqual("client write target engine", 2, writeEnvelope.TargetEngineId);
AssertEqual("client write command", NmxWriteMessage.Command, writeEnvelope.InnerBody.Span[0]);
AssertEqual("client write value kind", NmxWriteMessage.GetWireKind(MxValueKind.String), writeEnvelope.InnerBody.Span[NmxWriteMessage.KindOffset]);
AssertBytes(
"client write native ShortDesc body",
FromHex("37 01 00 05 00 36 d7 02 00 65 00 0a 00 6d 02 00 00 05 1e 00 00 00 1a 00 00 00 68 00 65 00 6c 00 6c 00 6f 00 2d 00 6e 00 61 00 74 00 69 00 76 00 65 00 00 00 ff ff 00 00 00 00 00 00 00 00 94 68 ff 0b 01 00 00 00"),
writeEnvelope.InnerBody.Span);
Guid correlationId = new("cd9ccac0-6532-46b0-a585-a583b2e77a5d");
byte[] adviseTransfer = ManagedNmxService2Client.EncodeAdviseSupervisoryTransferBody(
localEngineId: 0x7ffb,
tag,
correlationId);
var adviseEnvelope = NmxTransferEnvelope.Parse(adviseTransfer);
AssertEqual("client advise transfer kind", NmxTransferMessageKind.ItemControl, adviseEnvelope.MessageKind);
var advise = NmxItemControlMessage.Parse(adviseEnvelope.InnerBody.Span);
AssertEqual("client advise command", NmxItemControlCommand.AdviseSupervisory, advise.Command);
AssertEqual("client advise correlation", correlationId, advise.ItemCorrelationId);
AssertEqual("client advise property", (short)10, advise.PropertyId);
AssertEqual(
"compat suppress empty internationalized string",
true,
MxNativeCompatibilityServer.ShouldSuppressCompatibilityDataChange(tag, string.Empty));
AssertEqual(
"compat keep populated internationalized string",
false,
MxNativeCompatibilityServer.ShouldSuppressCompatibilityDataChange(tag, "populated"));
AssertEqual(
"compat keep normal empty string",
false,
MxNativeCompatibilityServer.ShouldSuppressCompatibilityDataChange(tag with { MxDataType = (short)MxDataType.String }, string.Empty));
var securedTag = new GalaxyTagMetadata(
ObjectTagName: "TestMachine_001",
AttributeName: "ProtectedValue",
PrimitiveName: null,
PlatformId: 1,
EngineId: 2,
ObjectId: 6,
PrimitiveId: 2,
AttributeId: 166,
PropertyId: 10,
MxDataType: (short)MxDataType.Boolean,
IsArray: false,
SecurityClassification: 2,
AttributeSource: "dynamic");
byte[] securedTransfer = ManagedNmxService2Client.EncodeWriteSecured2TransferBody(
localEngineId: 0x7ffa,
securedTag,
true,
new DateTime(2026, 4, 25, 8, 30, 0),
"MxManagedSecured2",
currentUserId: 1,
verifierUserId: 0);
var securedEnvelope = NmxTransferEnvelope.Parse(securedTransfer);
AssertEqual("client secured2 transfer kind", NmxTransferMessageKind.Write, securedEnvelope.MessageKind);
AssertEqual("client secured2 command", NmxSecuredWrite2Message.Command, securedEnvelope.InnerBody.Span[0]);
AssertEqual("client secured2 bool kind", NmxWriteMessage.GetWireKind(MxValueKind.Boolean), securedEnvelope.InnerBody.Span[NmxWriteMessage.KindOffset]);
}
static byte[] FromHex(string hex)
{
string[] parts = hex.Split(' ', StringSplitOptions.RemoveEmptyEntries);
byte[] bytes = new byte[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
bytes[i] = Convert.ToByte(parts[i], 16);
}
return bytes;
}
static ushort ReadUInt16(ReadOnlySpan<byte> buffer, int offset)
{
return (ushort)(buffer[offset] | (buffer[offset + 1] << 8));
}
static uint ReadUInt32(ReadOnlySpan<byte> buffer, int offset)
{
return (uint)(buffer[offset]
| (buffer[offset + 1] << 8)
| (buffer[offset + 2] << 16)
| (buffer[offset + 3] << 24));
}
static void WriteUInt32(Span<byte> buffer, int offset, uint value)
{
buffer[offset] = (byte)value;
buffer[offset + 1] = (byte)(value >> 8);
buffer[offset + 2] = (byte)(value >> 16);
buffer[offset + 3] = (byte)(value >> 24);
}
static void AssertEqual<T>(string name, T expected, T actual)
{
if (!EqualityComparer<T>.Default.Equals(expected, actual))
{
throw new InvalidOperationException($"{name}: expected {expected}, got {actual}.");
}
}
static void AssertBytes(string name, ReadOnlySpan<byte> expected, ReadOnlySpan<byte> actual)
{
if (!expected.SequenceEqual(actual))
{
throw new InvalidOperationException($"{name}: byte mismatch.");
}
}