fe2a6db786
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>
274 lines
8.0 KiB
C#
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;
|
|
}
|
|
}
|