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,314 @@
|
||||
//! `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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user