Files
mxaccess/rust/crates/mxaccess-codec/src/status.rs
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

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());
}
}