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>
315 lines
10 KiB
Rust
315 lines
10 KiB
Rust
//! `MxStatus` — 4-tuple `(Success, Category, DetectedBy, Detail)` per
|
|
//! `src/MxNativeCodec/MxStatus.cs:28-65`.
|
|
//!
|
|
//! `Success=-1` is the documented OK sentinel. Detail is a signed 16-bit
|
|
//! lookup code; canonical text for known codes is in [`detail_text`].
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
#[non_exhaustive]
|
|
#[repr(i16)]
|
|
pub enum MxStatusCategory {
|
|
#[default]
|
|
Unknown = -1,
|
|
Ok = 0,
|
|
Pending = 1,
|
|
Warning = 2,
|
|
CommunicationError = 3,
|
|
ConfigurationError = 4,
|
|
OperationalError = 5,
|
|
SecurityError = 6,
|
|
SoftwareError = 7,
|
|
OtherError = 8,
|
|
}
|
|
|
|
impl MxStatusCategory {
|
|
pub fn from_i16(value: i16) -> Self {
|
|
match value {
|
|
0 => Self::Ok,
|
|
1 => Self::Pending,
|
|
2 => Self::Warning,
|
|
3 => Self::CommunicationError,
|
|
4 => Self::ConfigurationError,
|
|
5 => Self::OperationalError,
|
|
6 => Self::SecurityError,
|
|
7 => Self::SoftwareError,
|
|
8 => Self::OtherError,
|
|
_ => Self::Unknown,
|
|
}
|
|
}
|
|
|
|
pub fn to_i16(self) -> i16 {
|
|
self as i16
|
|
}
|
|
}
|
|
|
|
/// Seven values per `MxStatus.cs:17-26`. The `DetectedBy` field is essential
|
|
/// for diagnostics — it identifies which layer detected the fault.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
#[non_exhaustive]
|
|
#[repr(i16)]
|
|
pub enum MxStatusSource {
|
|
#[default]
|
|
Unknown = -1,
|
|
RequestingLmx = 0,
|
|
RespondingLmx = 1,
|
|
RequestingNmx = 2,
|
|
RespondingNmx = 3,
|
|
RequestingAutomationObject = 4,
|
|
RespondingAutomationObject = 5,
|
|
}
|
|
|
|
impl MxStatusSource {
|
|
pub fn from_i16(value: i16) -> Self {
|
|
match value {
|
|
0 => Self::RequestingLmx,
|
|
1 => Self::RespondingLmx,
|
|
2 => Self::RequestingNmx,
|
|
3 => Self::RespondingNmx,
|
|
4 => Self::RequestingAutomationObject,
|
|
5 => Self::RespondingAutomationObject,
|
|
_ => Self::Unknown,
|
|
}
|
|
}
|
|
|
|
pub fn to_i16(self) -> i16 {
|
|
self as i16
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
pub struct MxStatus {
|
|
pub success: i16,
|
|
pub category: MxStatusCategory,
|
|
pub detected_by: MxStatusSource,
|
|
pub detail: i16,
|
|
}
|
|
|
|
impl MxStatus {
|
|
/// `(success=-1, Ok, RequestingLmx, detail=0)` — `MxStatus.DataChangeOk`
|
|
/// from `MxStatus.cs:36-40`.
|
|
pub const DATA_CHANGE_OK: Self = Self {
|
|
success: -1,
|
|
category: MxStatusCategory::Ok,
|
|
detected_by: MxStatusSource::RequestingLmx,
|
|
detail: 0,
|
|
};
|
|
|
|
/// `(success=-1, Ok, RespondingAutomationObject, detail=0)` —
|
|
/// `MxStatus.WriteCompleteOk` from `MxStatus.cs:42-46`.
|
|
pub const WRITE_COMPLETE_OK: Self = Self {
|
|
success: -1,
|
|
category: MxStatusCategory::Ok,
|
|
detected_by: MxStatusSource::RespondingAutomationObject,
|
|
detail: 0,
|
|
};
|
|
|
|
/// `(success=-1, Pending, RequestingLmx, detail=0)` —
|
|
/// `MxStatus.SuspendPending` from `MxStatus.cs:48-52`.
|
|
pub const SUSPEND_PENDING: Self = Self {
|
|
success: -1,
|
|
category: MxStatusCategory::Pending,
|
|
detected_by: MxStatusSource::RequestingLmx,
|
|
detail: 0,
|
|
};
|
|
|
|
/// `(success=-1, Ok, RequestingLmx, detail=0)` — `MxStatus.ActivateOk`
|
|
/// from `MxStatus.cs:54-58`.
|
|
pub const ACTIVATE_OK: Self = Self {
|
|
success: -1,
|
|
category: MxStatusCategory::Ok,
|
|
detected_by: MxStatusSource::RequestingLmx,
|
|
detail: 0,
|
|
};
|
|
|
|
/// `(success=0, ConfigurationError, RequestingLmx, detail=6)` —
|
|
/// `MxStatus.InvalidReferenceConfiguration` from `MxStatus.cs:60-64`.
|
|
pub const INVALID_REFERENCE_CONFIGURATION: Self = Self {
|
|
success: 0,
|
|
category: MxStatusCategory::ConfigurationError,
|
|
detected_by: MxStatusSource::RequestingLmx,
|
|
detail: 6,
|
|
};
|
|
|
|
/// Look up the canonical text for `self.detail`, mirroring
|
|
/// `MxStatus.DetailText` (`MxStatus.cs:34`). Returns `None` for unknown
|
|
/// detail codes.
|
|
pub fn detail_text(&self) -> Option<&'static str> {
|
|
detail_text(self.detail)
|
|
}
|
|
|
|
pub fn is_ok(&self) -> bool {
|
|
self.category == MxStatusCategory::Ok
|
|
}
|
|
}
|
|
|
|
/// Canonical detail-code text per `MxStatusDetails.KnownDetails`
|
|
/// (`MxStatus.cs:69-120`). Returns `None` for unknown codes.
|
|
pub fn detail_text(detail: i16) -> Option<&'static str> {
|
|
match detail {
|
|
16 => Some("Request timed out"),
|
|
17 => Some("Platform communication error"),
|
|
18 => Some("Invalid platform ID"),
|
|
19 => Some("Invalid engine ID"),
|
|
20 => Some("Engine communication error"),
|
|
21 => Some("Invalid reference"),
|
|
22 => Some("No Galaxy Repository"),
|
|
23 => Some("Invalid object ID"),
|
|
24 => Some("Object signature mismatch"),
|
|
25 => Some("Invalid primitive ID"),
|
|
26 => Some("Invalid attribute ID"),
|
|
27 => Some("Invalid property ID"),
|
|
28 => Some("Index out of range"),
|
|
29 => Some("Data out of range"),
|
|
30 => Some("Incorrect data type"),
|
|
31 => Some("Attribute not readable"),
|
|
32 => Some("Attribute not writeable"),
|
|
33 => Some("Write access denied"),
|
|
34 => Some("Unknown error"),
|
|
35 => Some("detected by"),
|
|
36 => Some("Wrong data type"),
|
|
37 => Some("Wrong number of dimensions"),
|
|
38 => Some("Invalid index"),
|
|
39 => Some("Index out of order"),
|
|
40 => Some("Dimension does not exist"),
|
|
41 => Some("Conversion not supported"),
|
|
42 => Some("Unable to convert string"),
|
|
43 => Some("Overflow"),
|
|
44 => Some("Attribute signature mismatch"),
|
|
45 => Some("Resolving local portion of reference"),
|
|
46 => Some("Resolving global portion of reference"),
|
|
47 => Some("Nmx version mismatch"),
|
|
48 => Some("Nmx command not valid"),
|
|
49 => Some("Lmx version mismatch"),
|
|
50 => Some("Lmx command not valid"),
|
|
51 => Some(
|
|
"However, the object could not be put On Scan - Permission to modify \"Operate\" attributes is required",
|
|
),
|
|
52 => Some(
|
|
"Unable to resolve reference for 'set' request because Galaxy Repository is busy performing a 'Deploy/Undeploy' operation",
|
|
),
|
|
53 => Some("Too many outstanding pending requests to engine"),
|
|
54 => Some("Object Initializing"),
|
|
55 => Some("Engine Initializing"),
|
|
56 => Some("Secured Write"),
|
|
57 => Some("Verified Write"),
|
|
58 => Some("No Alarm Ack Privilege"),
|
|
59 => Some("Alarm Acked Already"),
|
|
60 => Some("User did not have the necessary permissions to write"),
|
|
61 => Some("Verifier did not have the necessary permissions to verify"),
|
|
541 => Some("Conversion to intended data type is not supported"),
|
|
542 => Some("Unable to convert the input string to intended data type"),
|
|
8017 => Some(
|
|
"Object must be offscan to modify attributes that have an MxSecurityConfigure security classification",
|
|
),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn category_round_trip() {
|
|
for cat in [
|
|
MxStatusCategory::Unknown,
|
|
MxStatusCategory::Ok,
|
|
MxStatusCategory::Pending,
|
|
MxStatusCategory::Warning,
|
|
MxStatusCategory::CommunicationError,
|
|
MxStatusCategory::ConfigurationError,
|
|
MxStatusCategory::OperationalError,
|
|
MxStatusCategory::SecurityError,
|
|
MxStatusCategory::SoftwareError,
|
|
MxStatusCategory::OtherError,
|
|
] {
|
|
assert_eq!(MxStatusCategory::from_i16(cat.to_i16()), cat);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn source_round_trip() {
|
|
for src in [
|
|
MxStatusSource::Unknown,
|
|
MxStatusSource::RequestingLmx,
|
|
MxStatusSource::RespondingLmx,
|
|
MxStatusSource::RequestingNmx,
|
|
MxStatusSource::RespondingNmx,
|
|
MxStatusSource::RequestingAutomationObject,
|
|
MxStatusSource::RespondingAutomationObject,
|
|
] {
|
|
assert_eq!(MxStatusSource::from_i16(src.to_i16()), src);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_codes_map_to_unknown_variants() {
|
|
assert_eq!(MxStatusCategory::from_i16(99), MxStatusCategory::Unknown);
|
|
assert_eq!(MxStatusCategory::from_i16(-99), MxStatusCategory::Unknown);
|
|
assert_eq!(MxStatusSource::from_i16(99), MxStatusSource::Unknown);
|
|
assert_eq!(MxStatusSource::from_i16(-2), MxStatusSource::Unknown);
|
|
}
|
|
|
|
#[test]
|
|
fn canonical_sentinels_match_dotnet() {
|
|
// `MxStatus.cs:36-58` defines five canonical sentinels.
|
|
assert_eq!(MxStatus::DATA_CHANGE_OK.success, -1);
|
|
assert_eq!(MxStatus::DATA_CHANGE_OK.category, MxStatusCategory::Ok);
|
|
assert_eq!(
|
|
MxStatus::DATA_CHANGE_OK.detected_by,
|
|
MxStatusSource::RequestingLmx
|
|
);
|
|
assert_eq!(MxStatus::DATA_CHANGE_OK.detail, 0);
|
|
|
|
assert_eq!(
|
|
MxStatus::WRITE_COMPLETE_OK.detected_by,
|
|
MxStatusSource::RespondingAutomationObject
|
|
);
|
|
|
|
assert_eq!(
|
|
MxStatus::INVALID_REFERENCE_CONFIGURATION.success,
|
|
0,
|
|
"InvalidReferenceConfiguration uses success=0, not -1"
|
|
);
|
|
assert_eq!(MxStatus::INVALID_REFERENCE_CONFIGURATION.detail, 6);
|
|
}
|
|
|
|
#[test]
|
|
fn detail_text_known_codes() {
|
|
assert_eq!(detail_text(16), Some("Request timed out"));
|
|
assert_eq!(detail_text(21), Some("Invalid reference"));
|
|
assert_eq!(detail_text(33), Some("Write access denied"));
|
|
assert_eq!(detail_text(57), Some("Verified Write"));
|
|
assert_eq!(
|
|
detail_text(541),
|
|
Some("Conversion to intended data type is not supported")
|
|
);
|
|
assert_eq!(
|
|
detail_text(8017),
|
|
Some(
|
|
"Object must be offscan to modify attributes that have an MxSecurityConfigure security classification"
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn detail_text_unknown_codes() {
|
|
assert_eq!(detail_text(0), None);
|
|
assert_eq!(detail_text(15), None);
|
|
assert_eq!(detail_text(62), None);
|
|
assert_eq!(detail_text(540), None);
|
|
assert_eq!(detail_text(8018), None);
|
|
assert_eq!(detail_text(-1), None);
|
|
}
|
|
|
|
#[test]
|
|
fn is_ok_categorisation() {
|
|
assert!(MxStatus::DATA_CHANGE_OK.is_ok());
|
|
assert!(MxStatus::WRITE_COMPLETE_OK.is_ok());
|
|
assert!(MxStatus::ACTIVATE_OK.is_ok());
|
|
assert!(!MxStatus::SUSPEND_PENDING.is_ok());
|
|
assert!(!MxStatus::INVALID_REFERENCE_CONFIGURATION.is_ok());
|
|
}
|
|
}
|