Files
mxaccess/src/MxAsbClient/AsbResultMapping.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

274 lines
8.0 KiB
C#

namespace MxAsbClient;
public enum AsbErrorCode : ushort
{
Success = 0,
InvalidConnectionId = 1,
ApplicationAuthenticationError = 2,
UserAuthenticationError = 3,
UserAuthorizationError = 4,
NotSupportedOperation = 5,
MonitoredItemsNotFound = 6,
InvalidSubscriptionId = 7,
ItemAlreadyRegistered = 8,
ItemAlreadyDeletedOrDoesNotExist = 9,
InvalidMonitoredItems = 10,
OperationFailed = 11,
SpecificError = 12,
BadNoCommunication = 13,
BadNothingToDo = 14,
BadTooManyOperations = 15,
BadNodeIdInvalid = 16,
BrowseFailed = 17,
WriteFailedBadOutOfRange = 18,
WriteFailedBadTypeMismatch = 19,
WriteFailedBadDimensionMismatch = 20,
WriteFailedAccessDenied = 21,
WriteFailedSecuredWrite = 22,
WriteFailedVerifiedWrite = 23,
IndexOutOfRange = 24,
RequestTimedOut = 25,
DataTypeConversionNotSupported = 26,
ItemCannotBeRegisteredNoName = 27,
ItemCannotBeRegisteredNoId = 28,
ItemAlreadyBeingMonitored = 29,
SubscriptionIdAlreadyExist = 30,
OperationWouldBlock = 31,
PublishComplete = 32,
WriteFailedUserNotHavingAccessRights = 33,
WriteFailedVerifierNotHavingVerifyRights = 34,
ObjectNotInitialized = 128,
EndPointNotFound = 129,
ConnectionClosed = 130,
InvalidParameter = 131,
MemoryAllocationError = 132,
OperationNotComplete = 133,
FileOperationFailed = 256,
InvalidXmlFile = 272,
RecordLookupError = 288,
Unknown = ushort.MaxValue,
}
public enum AsbStatusQuality
{
Unknown = 0,
Bad = 1,
Uncertain = 2,
Good = 3,
}
public enum AsbMxStatusCategory
{
Unknown = -1,
Ok = 0,
Pending = 1,
Warning = 2,
CommunicationError = 3,
ConfigurationError = 4,
OperationalError = 5,
SecurityError = 6,
SoftwareError = 7,
OtherError = 8,
}
public enum AsbMxStatusDetail
{
Unknown = -1,
None = 0,
RequestTimedOut = 16,
PlatformCommunicationError = 17,
WriteAccessDenied = 33,
}
public sealed record AsbResultSummary(
AsbErrorCode Error,
int RawErrorCode,
uint Status,
uint SpecificErrorCode,
bool IsSuccess,
bool IsSuccessLike);
public sealed record AsbItemStatusSummary(
string? ItemName,
ulong ItemId,
AsbErrorCode Error,
ushort RawErrorCode,
bool IsSuccess,
IReadOnlyList<AsbStatusElement> Status)
{
public AsbStatusSummary StatusSummary { get; init; } = AsbResultMapper.ToStatusSummary(Status);
}
public sealed record AsbStatusSummary(
ushort? RawQuality,
AsbStatusQuality Quality,
ushort? RawCategory,
AsbMxStatusCategory Category,
ushort? RawDetail,
AsbMxStatusDetail Detail,
AsbErrorCode? StatusError,
bool IsGoodQuality,
bool IsSuccessLike,
IReadOnlyList<AsbStatusElement> Elements);
public static class AsbResultMapper
{
public static AsbResultSummary ToSummary(ArchestrAResult result)
{
AsbErrorCode error = result.ErrorCode is >= 0 and <= ushort.MaxValue
? ToErrorCode((ushort)result.ErrorCode)
: AsbErrorCode.Unknown;
bool isSuccess = error == AsbErrorCode.Success || result.Success;
return new AsbResultSummary(
error,
result.ErrorCode,
result.Status,
result.SpecificErrorCode,
isSuccess,
isSuccess || error == AsbErrorCode.PublishComplete);
}
public static AsbItemStatusSummary ToItemSummary(ItemStatus status)
{
AsbErrorCode error = ToErrorCode(status.ErrorCode);
IReadOnlyList<AsbStatusElement> elements = AsbPublishMapper.DecodeStatus(status.Status);
return new AsbItemStatusSummary(
status.Item.Name,
status.Item.Id,
error,
status.ErrorCode,
error == AsbErrorCode.Success,
elements);
}
public static IReadOnlyList<AsbItemStatusSummary> ToItemSummaries(ItemStatus[]? statuses)
{
if (statuses is null || statuses.Length == 0)
{
return [];
}
AsbItemStatusSummary[] summaries = new AsbItemStatusSummary[statuses.Length];
for (int i = 0; i < statuses.Length; i++)
{
summaries[i] = ToItemSummary(statuses[i]);
}
return summaries;
}
public static AsbErrorCode ToErrorCode(ushort errorCode)
{
return Enum.IsDefined(typeof(AsbErrorCode), errorCode)
? (AsbErrorCode)errorCode
: AsbErrorCode.Unknown;
}
public static AsbStatusSummary ToStatusSummary(AsbStatus status)
{
return ToStatusSummary(AsbPublishMapper.DecodeStatus(status));
}
public static AsbStatusSummary ToStatusSummary(IReadOnlyList<AsbStatusElement> elements)
{
ushort? rawQuality = FirstValue(elements, AsbStatusElementType.MxQuality);
ushort? rawCategory = FirstValue(elements, AsbStatusElementType.MxStatusCategory);
ushort? rawDetail = FirstValue(elements, AsbStatusElementType.MxStatusDetail);
AsbStatusQuality quality = ToQuality(rawQuality);
AsbMxStatusCategory category = ToMxStatusCategory(rawCategory);
AsbMxStatusDetail detail = ToMxStatusDetail(rawDetail);
AsbErrorCode? statusError = ToStatusError(category, detail);
bool isGoodQuality = quality == AsbStatusQuality.Good;
bool isSuccessLike = statusError == AsbErrorCode.Success
&& (quality == AsbStatusQuality.Good || quality == AsbStatusQuality.Unknown);
return new AsbStatusSummary(
rawQuality,
quality,
rawCategory,
category,
rawDetail,
detail,
statusError,
isGoodQuality,
isSuccessLike,
elements);
}
public static AsbStatusQuality ToQuality(ushort? quality)
{
if (quality is null)
{
return AsbStatusQuality.Unknown;
}
return (quality.Value & 0x00C0) switch
{
0x00C0 => AsbStatusQuality.Good,
0x0040 => AsbStatusQuality.Uncertain,
0x0000 => AsbStatusQuality.Bad,
_ => AsbStatusQuality.Unknown,
};
}
public static AsbMxStatusCategory ToMxStatusCategory(ushort? category)
{
return category switch
{
0 => AsbMxStatusCategory.Ok,
1 => AsbMxStatusCategory.Pending,
2 => AsbMxStatusCategory.Warning,
3 => AsbMxStatusCategory.CommunicationError,
4 => AsbMxStatusCategory.ConfigurationError,
5 => AsbMxStatusCategory.OperationalError,
6 => AsbMxStatusCategory.SecurityError,
7 => AsbMxStatusCategory.SoftwareError,
8 => AsbMxStatusCategory.OtherError,
_ => AsbMxStatusCategory.Unknown,
};
}
public static AsbMxStatusDetail ToMxStatusDetail(ushort? detail)
{
return detail switch
{
0 => AsbMxStatusDetail.None,
16 => AsbMxStatusDetail.RequestTimedOut,
17 => AsbMxStatusDetail.PlatformCommunicationError,
33 => AsbMxStatusDetail.WriteAccessDenied,
_ => AsbMxStatusDetail.Unknown,
};
}
private static AsbErrorCode? ToStatusError(AsbMxStatusCategory category, AsbMxStatusDetail detail)
{
if (category == AsbMxStatusCategory.Ok && detail == AsbMxStatusDetail.None)
{
return AsbErrorCode.Success;
}
return detail switch
{
AsbMxStatusDetail.RequestTimedOut => AsbErrorCode.RequestTimedOut,
AsbMxStatusDetail.PlatformCommunicationError => AsbErrorCode.BadNoCommunication,
AsbMxStatusDetail.WriteAccessDenied => AsbErrorCode.WriteFailedAccessDenied,
_ => null,
};
}
private static ushort? FirstValue(IReadOnlyList<AsbStatusElement> elements, AsbStatusElementType type)
{
foreach (AsbStatusElement element in elements)
{
if (element.Type == type)
{
return element.Value;
}
}
return null;
}
}