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,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>
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user