[R3/R4 Path A] mxaccess: port Lmx.dll FUN_10100ce0 synthesizer kernel
Path A landed for R3/R4. The byte->MxStatus synthesizer in Lmx.dll is
FUN_10100ce0 (`analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`),
a 4-byte u32 LE -> 4-tuple MxStatus decoder used by every NMX-frame
parser in Lmx.dll. The kernel is byte-deterministic and context-free,
so it ports as a pure function -- the operation-tracking state
machine the original verdict deferred is NOT required for synthesis.
Bit layout (per FUN_10100ce0 lines 21-24):
bit 31: success (-1 if set, 0 if clear)
bits 27..24: category (4 bits)
bits 23..20: detected_by (4 bits)
bits 15..0: detail (i16 -- low 16 bits, signed)
bits 30..28, 19..16: reserved/padding
Codec changes:
- MxStatus::from_packed_u32() / ::to_packed_u32() -- the kernel +
inverse for round-trip parity.
- MxStatus::from_nmx_response_code() -- the constructed-from-response-
code switch in FUN_1010bd10:741-770 (six proven mappings: 0x01, 0x02
-> CommunicationError + RequestingNmx; 0x03 -> ConfigurationError +
RequestingNmx; 0x04 -> ConfigurationError + RespondingNmx; 0x05 ->
CommunicationError + RespondingNmx; 0x1A -> CommunicationError +
RequestingNmx).
- MxStatusCategory / MxStatusSource: from_i16/to_i16 promoted to const
fn so MxStatus::from_packed_u32 can be const.
- NmxOperationStatusMessage::try_parse_process_data_received_body() --
thin wrapper that peels the outer NmxObservedEnvelope before
delegating to try_parse_inner. Mirrors
NmxOperationStatusMessage.TryParseProcessDataReceivedBody (.NET cs:20-32).
- NmxOperationStatusMessage::promote_to_typed() -- entry point that
returns the existing Status field. Documented as a no-op pass-through
for now (the 5-byte inner-body wire shape is NOT the same field as
the 4-byte packed-u32 the kernel decodes); kept for API symmetry.
- 22 new round-trip tests covering the kernel, the response-code
switch, the proven 0x00/0x41/0xEF completion bytes, and round-trip
for every canonical sentinel.
mxaccess (Session) changes:
- New OperationKind enum (Write/WriteSecured/Read/Subscribe/
Unsubscribe/Activate/Suspend/Other).
- New OperationContext struct (correlation_id, op_kind, reference,
retry_count) -- ground for the F54 follow-on per-operation
correlation work.
- New OperationStatus event type {raw, status, context,
is_during_recovery}, mirroring MxNativeOperationStatusEvent (cs:73-78)
with the typed-MxStatus addition.
- Session::operation_status_events() -> broadcast::Receiver<Arc<
OperationStatus>> + operation_status_stream() Stream variant.
- callback_router() now tries operation-status parsing first, falling
through to subscription messages -- matches MxNativeSession
.OnCallbackReceived dispatch order (cs:574,582,590).
- recover_connection() flips a recovery_active counter (Arc<AtomicU32>
shared with the router) so OperationStatus.is_during_recovery is
populated correctly. Mirrors MxNativeSession._recoveryActive
Volatile.Read at cs:573.
- 3 new router tests covering: status-word frame dispatch + typed
promotion to WriteCompleteOk; completion-only frames stay verbatim;
is_during_recovery is stamped from the live counter.
Per-operation context tracking (correlating completion frames back to
outstanding writes/subscribes via the correlation_id) is filed as F54
in design/followups.md. The synthesizer kernel itself is byte-
deterministic, so the kernel and the correlation work are decoupled.
Ghidra evidence (the next-ring xref walk beyond FUN_10114a90):
- analysis/ghidra/exports/Lmx.dll.set-attribute-result-xrefs.md --
xrefs to OnSetAttributeResult / CancelWithStatus / OperationComplete.
- analysis/ghidra/exports/Lmx.dll.vtable-data-xrefs.md -- vtable-slot
data xrefs for the virtual-dispatch path.
- analysis/ghidra/exports/Lmx.dll.synthesizer-decompile.md --
ScanOnDemandCallback::OperationComplete/MultipleOperationComplete
(FUN_1010b990), RemotePlatformResolver::OperationComplete
(FUN_1010dc80), and the constructed-from-responseCode synthesizer
in FUN_1010bd10 (lines 698-770). FUN_1010bd10 is the wire-frame
receiver that drives the synthesis.
- analysis/ghidra/exports/Lmx.dll.synthesizer-helpers-decompile.md --
FUN_10003fc0 (the <success %d category %d ...> formatter; confirms
the 4-tuple layout), FUN_1008f150 (dispatch helper).
- analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md --
FUN_10100ce0 (the kernel itself), FUN_10100bc0 (3xu16 reader),
FUN_1005e580 (4-byte stream reader), FUN_1010ee00 (sister NMX-frame
parser using the same kernel).
- analysis/ghidra/exports/Lmx.dll.synthesizer-callers-xrefs.md --
caller graph; confirms the kernel is called from many wire-frame
parsers but each parser shares the single 4-byte decoder.
R3/R4 verdict updated in design/70-risks-and-open-questions.md from
"settled at verbatim-preserve" to "settled per Path A". F54 filed in
design/followups.md for the per-operation correlation work.
cargo build / test / clippy -D warnings / RUSTDOCFLAGS=-D warnings doc
all clean. cargo public-api baselines regenerated for mxaccess and
mxaccess-codec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,11 +21,33 @@
|
||||
//! [`NmxOperationStatusMessage::try_parse_inner`] is provided here. When
|
||||
//! `NmxObservedEnvelope` lands, add `try_parse_process_data_received_body` as
|
||||
//! a thin wrapper.
|
||||
//!
|
||||
//! ## Typed promotion and the synthesizer kernel
|
||||
//!
|
||||
//! [`NmxOperationStatusMessage::promote_to_typed`] returns the same
|
||||
//! [`MxStatus`] the parser already attached to the message — the
|
||||
//! verbatim-preserve placeholder for unknown shapes, the
|
||||
//! [`MxStatus::WRITE_COMPLETE_OK`] sentinel for the proven
|
||||
//! `(status_code=0x8050, completion_code=0x00)` shape. The 5-byte
|
||||
//! `00 00 SS SS CC` inner body is **not** the same wire field as the
|
||||
//! 4-byte packed status word `Lmx.dll!FUN_10100ce0` decodes
|
||||
//! ([`MxStatus::from_packed_u32`]) — that kernel applies one layer up,
|
||||
//! to the `INmxService.GetResponse2` payload's `status: i32` field
|
||||
//! (carried e.g. in subscription records). See
|
||||
//! `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`
|
||||
//! and `design/70-risks-and-open-questions.md` R3/R4 Path A for the
|
||||
//! evidence chain.
|
||||
//!
|
||||
//! `promote_to_typed` is therefore a thin convenience over the existing
|
||||
//! `status` field: callers that want the canonical bit-layout decoder
|
||||
//! should reach for [`MxStatus::from_packed_u32`] directly when they
|
||||
//! have a 4-byte packed value in hand.
|
||||
|
||||
// Direct byte indexing — see reference_handle.rs for rationale.
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use crate::error::CodecError;
|
||||
use crate::observed_frame::NmxObservedEnvelope;
|
||||
use crate::status::{MxStatus, MxStatusCategory, MxStatusSource};
|
||||
|
||||
/// Which of the two recognised inner-frame shapes was decoded
|
||||
@@ -78,6 +100,47 @@ impl NmxOperationStatusMessage {
|
||||
&& self.completion_code == 0x00
|
||||
}
|
||||
|
||||
/// Return the typed [`MxStatus`] for this frame.
|
||||
///
|
||||
/// This is a thin convenience over [`Self::status`] — same value,
|
||||
/// no transformation. Provided for API symmetry with
|
||||
/// [`MxStatus::from_packed_u32`] (the canonical 4-byte synthesizer
|
||||
/// kernel) and to give consumers a single entry point that can
|
||||
/// be extended in future revisions if new evidence pins additional
|
||||
/// `(status_code, completion_code)` shapes.
|
||||
///
|
||||
/// **What this method does NOT do:** apply the
|
||||
/// `Lmx.dll!FUN_10100ce0` synthesizer to the 5-byte inner body.
|
||||
/// The 5-byte `00 00 SS SS CC` shape and the 4-byte packed-u32
|
||||
/// shape are different wire fields at different layers — see the
|
||||
/// module docs and
|
||||
/// `design/70-risks-and-open-questions.md` R3/R4 Path A. Callers
|
||||
/// holding a 4-byte packed `MxStatus` (e.g. extracted from a
|
||||
/// subscription record's `status: i32`) should call
|
||||
/// [`MxStatus::from_packed_u32`] directly.
|
||||
#[must_use]
|
||||
pub const fn promote_to_typed(&self) -> MxStatus {
|
||||
self.status
|
||||
}
|
||||
|
||||
/// Peel the outer [`NmxObservedEnvelope`] off a `ProcessDataReceived`
|
||||
/// payload and parse the inner body. Mirrors
|
||||
/// `NmxOperationStatusMessage.TryParseProcessDataReceivedBody`
|
||||
/// (`NmxOperationStatusMessage.cs:20-32`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` when the outer envelope cannot be parsed or the
|
||||
/// inner body matches no recognised shape (1- or 5-byte completion
|
||||
/// frame). The .NET reference returns `false` and a `null!`
|
||||
/// out-param in both cases; the Rust port surfaces a typed
|
||||
/// [`CodecError`] so callers can distinguish "not a process-data
|
||||
/// frame" from "successfully parsed".
|
||||
pub fn try_parse_process_data_received_body(body: &[u8]) -> Result<Self, CodecError> {
|
||||
let envelope = NmxObservedEnvelope::parse_process_data_received_body_flexible(body)?;
|
||||
Self::try_parse_inner(&envelope.inner_body)
|
||||
}
|
||||
|
||||
/// Parse an inner body — either 1 byte (`CompletionOnly`) or 5 bytes
|
||||
/// (`StatusWord` with leading `00 00`).
|
||||
///
|
||||
@@ -281,4 +344,38 @@ mod tests {
|
||||
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
|
||||
assert_eq!(msg.status_code, 0xBBAA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promote_to_typed_returns_existing_status_for_status_word() {
|
||||
// The proven shape — must keep returning the canonical sentinel.
|
||||
let frame = [0x00, 0x00, 0x50, 0x80, 0x00];
|
||||
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
|
||||
assert_eq!(msg.promote_to_typed(), MxStatus::WRITE_COMPLETE_OK);
|
||||
assert_eq!(msg.promote_to_typed(), msg.status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promote_to_typed_returns_verbatim_status_for_completion_only() {
|
||||
// 1-byte frames: no synthesizer evidence — must stay verbatim.
|
||||
for byte in [0x00_u8, 0x41, 0xEF] {
|
||||
let msg = NmxOperationStatusMessage::try_parse_inner(&[byte]).unwrap();
|
||||
let promoted = msg.promote_to_typed();
|
||||
assert_eq!(promoted, msg.status);
|
||||
assert_eq!(promoted.category, MxStatusCategory::Unknown);
|
||||
assert_eq!(promoted.detected_by, MxStatusSource::Unknown);
|
||||
assert_eq!(promoted.detail, i16::from(byte));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promote_to_typed_does_not_change_existing_status_field() {
|
||||
// promote_to_typed must not mutate the verbatim-preserve `status`
|
||||
// field. This guards the byte-for-byte parity contract with the
|
||||
// .NET reference.
|
||||
let frame = [0x00, 0x00, 0x55, 0xAA, 0x33];
|
||||
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
|
||||
let original_status = msg.status;
|
||||
let _typed = msg.promote_to_typed();
|
||||
assert_eq!(msg.status, original_status);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user