[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ pub enum MxStatusCategory {
|
||||
}
|
||||
|
||||
impl MxStatusCategory {
|
||||
pub fn from_i16(value: i16) -> Self {
|
||||
pub const fn from_i16(value: i16) -> Self {
|
||||
match value {
|
||||
0 => Self::Ok,
|
||||
1 => Self::Pending,
|
||||
@@ -37,7 +37,7 @@ impl MxStatusCategory {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_i16(self) -> i16 {
|
||||
pub const fn to_i16(self) -> i16 {
|
||||
self as i16
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ pub enum MxStatusSource {
|
||||
}
|
||||
|
||||
impl MxStatusSource {
|
||||
pub fn from_i16(value: i16) -> Self {
|
||||
pub const fn from_i16(value: i16) -> Self {
|
||||
match value {
|
||||
0 => Self::RequestingLmx,
|
||||
1 => Self::RespondingLmx,
|
||||
@@ -71,7 +71,7 @@ impl MxStatusSource {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_i16(self) -> i16 {
|
||||
pub const fn to_i16(self) -> i16 {
|
||||
self as i16
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,135 @@ pub struct MxStatus {
|
||||
}
|
||||
|
||||
impl MxStatus {
|
||||
/// Decode a 4-byte packed `MxStatus` word.
|
||||
///
|
||||
/// Mirrors the canonical NMX wire-frame status decoder
|
||||
/// `Lmx.dll!FUN_10100ce0` (see
|
||||
/// `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`).
|
||||
/// That function reads 4 bytes from a stream into a u32 and unpacks
|
||||
/// them via the bit layout:
|
||||
///
|
||||
/// ```text
|
||||
/// bit 31: success (-1 if set, 0 if clear)
|
||||
/// bits 27..24: category (4 bits, masked by 0xF)
|
||||
/// bits 23..20: detected_by (4 bits, masked by 0xF)
|
||||
/// bits 15..0: detail (i16 — low 16 bits, signed)
|
||||
/// bits 30..28, 19..16: reserved/padding (ignored)
|
||||
/// ```
|
||||
///
|
||||
/// This is the **synthesizer kernel** documented in
|
||||
/// `design/70-risks-and-open-questions.md` R3/R4 Path A. Every NMX
|
||||
/// wire frame that carries a status word emits one of these 4-byte
|
||||
/// packings; the consumer-side dispatch (retry counters, callback
|
||||
/// fan-out) is layered on top of the decoded `MxStatus`, but the
|
||||
/// decoder itself is byte-deterministic and context-free.
|
||||
///
|
||||
/// The `success` field is normalized to either `0` or `-1` per the
|
||||
/// native `Lmx.dll` semantics: any value with bit 31 set decodes to
|
||||
/// `-1`, any value with bit 31 clear decodes to `0`. (Native code:
|
||||
/// `*param_1 = -(ushort)(((uint)param_2 & 0x80000000) != 0)`.)
|
||||
///
|
||||
/// Unknown category / detected_by codes (i.e. a 4-bit value that
|
||||
/// does not match a documented [`MxStatusCategory`] /
|
||||
/// [`MxStatusSource`] variant) decode to the corresponding
|
||||
/// `Unknown` variant. The padding bits are silently discarded.
|
||||
#[must_use]
|
||||
pub const fn from_packed_u32(packed: u32) -> Self {
|
||||
// Bit layout — see fn doc.
|
||||
let success: i16 = if packed & 0x8000_0000 != 0 { -1 } else { 0 };
|
||||
let category_bits = ((packed >> 24) & 0xF) as i16;
|
||||
let detected_by_bits = ((packed >> 20) & 0xF) as i16;
|
||||
let detail = packed as i16;
|
||||
Self {
|
||||
success,
|
||||
category: MxStatusCategory::from_i16(category_bits),
|
||||
detected_by: MxStatusSource::from_i16(detected_by_bits),
|
||||
detail,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an `MxStatus` from a single-byte NMX response code.
|
||||
///
|
||||
/// Mirrors the synthesis switch in
|
||||
/// `Lmx.dll!FUN_1010bd10` (`ScanOnDemandCallback::GetResponse`)
|
||||
/// at lines 741-770 of
|
||||
/// `analysis/ghidra/exports/Lmx.dll.synthesizer-decompile.md`.
|
||||
/// When the NMX `responseCode` is non-zero (no payload status word
|
||||
/// to parse), `Lmx.dll` constructs an `MxStatus` from the response
|
||||
/// code itself using this fixed mapping:
|
||||
///
|
||||
/// | responseCode | category | detected_by |
|
||||
/// |---|---|---|
|
||||
/// | `0x01`, `0x02` | `CommunicationError` | `RequestingNmx` |
|
||||
/// | `0x03` | `ConfigurationError` | `RequestingNmx` |
|
||||
/// | `0x04` | `ConfigurationError` | `RespondingNmx` |
|
||||
/// | `0x05` | `CommunicationError` | `RespondingNmx` |
|
||||
/// | `0x1A` | `CommunicationError` | `RequestingNmx` |
|
||||
///
|
||||
/// `success` is `0` (not `-1`) and `detail` carries the response
|
||||
/// code unchanged. Unmapped codes return `None` — the native code's
|
||||
/// `default` branch leaves the synthesized status untouched, so the
|
||||
/// caller falls back to a verbatim raw-byte placeholder per
|
||||
/// `design/70-risks-and-open-questions.md` R3/R4.
|
||||
///
|
||||
/// This is **not** the same wire field as the 1-byte completion
|
||||
/// frames `0x00`/`0x41`/`0xEF` parsed by
|
||||
/// [`crate::NmxOperationStatusMessage::try_parse_inner`]: those
|
||||
/// live inside a `0x32`/`0x33` callback body, while this
|
||||
/// `responseCode` is the second `out` parameter of
|
||||
/// `INmxService.GetResponse2(...)` (one layer up the stack).
|
||||
/// `Lmx.dll`'s decoder for the 1-byte completion frames does not
|
||||
/// apply this synthesis.
|
||||
#[must_use]
|
||||
pub const fn from_nmx_response_code(response_code: u8) -> Option<Self> {
|
||||
// Per `FUN_1010bd10:741-770` switch.
|
||||
let (category, detected_by) = match response_code {
|
||||
0x01 | 0x02 => (
|
||||
MxStatusCategory::CommunicationError,
|
||||
MxStatusSource::RequestingNmx,
|
||||
),
|
||||
0x03 => (
|
||||
MxStatusCategory::ConfigurationError,
|
||||
MxStatusSource::RequestingNmx,
|
||||
),
|
||||
0x04 => (
|
||||
MxStatusCategory::ConfigurationError,
|
||||
MxStatusSource::RespondingNmx,
|
||||
),
|
||||
0x05 => (
|
||||
MxStatusCategory::CommunicationError,
|
||||
MxStatusSource::RespondingNmx,
|
||||
),
|
||||
0x1A => (
|
||||
MxStatusCategory::CommunicationError,
|
||||
MxStatusSource::RequestingNmx,
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Self {
|
||||
success: 0,
|
||||
category,
|
||||
detected_by,
|
||||
detail: response_code as i16,
|
||||
})
|
||||
}
|
||||
|
||||
/// Pack `self` back into the 4-byte NMX wire layout. Inverse of
|
||||
/// [`Self::from_packed_u32`]. Useful for round-trip tests and
|
||||
/// future encoder paths.
|
||||
///
|
||||
/// Padding bits (30..28, 19..16) are emitted as zero. Bit 31 mirrors
|
||||
/// `success != 0` — any non-zero `success` round-trips to `-1`
|
||||
/// because the decoder normalizes to `0`/`-1` only.
|
||||
#[must_use]
|
||||
pub const fn to_packed_u32(self) -> u32 {
|
||||
let success_bit: u32 = if self.success != 0 { 0x8000_0000 } else { 0 };
|
||||
let category_bits = ((self.category as i16) as u32 & 0xF) << 24;
|
||||
let detected_by_bits = ((self.detected_by as i16) as u32 & 0xF) << 20;
|
||||
let detail_bits = (self.detail as u16) as u32;
|
||||
success_bit | category_bits | detected_by_bits | detail_bits
|
||||
}
|
||||
|
||||
/// `(success=-1, Ok, RequestingLmx, detail=0)` — `MxStatus.DataChangeOk`
|
||||
/// from `MxStatus.cs:36-40`.
|
||||
pub const DATA_CHANGE_OK: Self = Self {
|
||||
@@ -311,4 +440,199 @@ mod tests {
|
||||
assert!(!MxStatus::SUSPEND_PENDING.is_ok());
|
||||
assert!(!MxStatus::INVALID_REFERENCE_CONFIGURATION.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_zero_decodes_to_all_zeros() {
|
||||
// packed=0 → success=0, category=Ok(0), detected_by=RequestingLmx(0), detail=0.
|
||||
// The "all zeros" status is the simplest data-change-pending shape
|
||||
// the wire can carry.
|
||||
let s = MxStatus::from_packed_u32(0);
|
||||
assert_eq!(s.success, 0);
|
||||
assert_eq!(s.category, MxStatusCategory::Ok);
|
||||
assert_eq!(s.detected_by, MxStatusSource::RequestingLmx);
|
||||
assert_eq!(s.detail, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_high_bit_sets_success_to_negative_one() {
|
||||
// Native: `*param_1 = -(ushort)(((uint)param_2 & 0x80000000) != 0)`
|
||||
// For packed=0x80000000, success=-1, all other fields 0.
|
||||
let s = MxStatus::from_packed_u32(0x8000_0000);
|
||||
assert_eq!(s.success, -1);
|
||||
assert_eq!(s.category, MxStatusCategory::Ok);
|
||||
assert_eq!(s.detected_by, MxStatusSource::RequestingLmx);
|
||||
assert_eq!(s.detail, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_decodes_data_change_ok_layout() {
|
||||
// `MxStatus::DATA_CHANGE_OK` = (success=-1, Ok=0, RequestingLmx=0,
|
||||
// detail=0). Pack: bit31=1, bits27..24=0, bits23..20=0, bits15..0=0.
|
||||
// → 0x80000000.
|
||||
let packed = MxStatus::DATA_CHANGE_OK.to_packed_u32();
|
||||
assert_eq!(packed, 0x8000_0000);
|
||||
let round_trip = MxStatus::from_packed_u32(packed);
|
||||
assert_eq!(round_trip, MxStatus::DATA_CHANGE_OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_decodes_write_complete_ok_layout() {
|
||||
// `MxStatus::WRITE_COMPLETE_OK` = (success=-1, Ok=0,
|
||||
// RespondingAutomationObject=5, detail=0). Pack: bit31=1,
|
||||
// bits27..24=0 (Ok), bits23..20=5, bits15..0=0.
|
||||
// → 0x80500000.
|
||||
let expected_packed: u32 = 0x80_50_00_00;
|
||||
let s = MxStatus::from_packed_u32(expected_packed);
|
||||
assert_eq!(s, MxStatus::WRITE_COMPLETE_OK);
|
||||
assert_eq!(MxStatus::WRITE_COMPLETE_OK.to_packed_u32(), expected_packed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_extracts_category_from_bits_24_to_27() {
|
||||
// category=4 (ConfigurationError) at bits 24..27.
|
||||
// → 0x04000000.
|
||||
let s = MxStatus::from_packed_u32(0x0400_0000);
|
||||
assert_eq!(s.category, MxStatusCategory::ConfigurationError);
|
||||
assert_eq!(s.detected_by, MxStatusSource::RequestingLmx);
|
||||
assert_eq!(s.detail, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_extracts_detected_by_from_bits_20_to_23() {
|
||||
// detected_by=2 (RequestingNmx) at bits 20..23.
|
||||
// → 0x00200000.
|
||||
let s = MxStatus::from_packed_u32(0x0020_0000);
|
||||
assert_eq!(s.category, MxStatusCategory::Ok);
|
||||
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
|
||||
assert_eq!(s.detail, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_extracts_detail_as_signed_low_16_bits() {
|
||||
// detail=21 ("Invalid reference") at bits 0..15.
|
||||
// → 0x00000015.
|
||||
let s = MxStatus::from_packed_u32(0x0000_0015);
|
||||
assert_eq!(s.detail, 21);
|
||||
assert_eq!(s.detail_text(), Some("Invalid reference"));
|
||||
|
||||
// Negative detail — high bit of low-16 set: 0xFFFF → -1.
|
||||
let s = MxStatus::from_packed_u32(0x0000_FFFF);
|
||||
assert_eq!(s.detail, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_padding_bits_are_ignored() {
|
||||
// Bits 30..28 and 19..16 are padding/reserved per `FUN_10100ce0`.
|
||||
// Setting them should not affect any decoded field.
|
||||
// bit 31: success
|
||||
// bits 30..28: padding (0x70_00_00_00)
|
||||
// bits 27..24: category
|
||||
// bits 23..20: detected_by
|
||||
// bits 19..16: padding (0x00_0F_00_00)
|
||||
// bits 15..0: detail
|
||||
// Padding-only mask: 0x70_00_00_00 | 0x00_0F_00_00 = 0x700F_0000.
|
||||
let with_padding = MxStatus::from_packed_u32(0x700F_0000);
|
||||
let without_padding = MxStatus::from_packed_u32(0x0000_0000);
|
||||
assert_eq!(with_padding, without_padding);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_unknown_category_decodes_to_unknown_variant() {
|
||||
// Category bits = 0xF (not a defined variant).
|
||||
// → 0x0F000000.
|
||||
let s = MxStatus::from_packed_u32(0x0F00_0000);
|
||||
assert_eq!(s.category, MxStatusCategory::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_packed_u32_unknown_detected_by_decodes_to_unknown_variant() {
|
||||
// detected_by bits = 0xF (not a defined variant).
|
||||
// → 0x00F00000.
|
||||
let s = MxStatus::from_packed_u32(0x00F0_0000);
|
||||
assert_eq!(s.detected_by, MxStatusSource::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_canonical_sentinels() {
|
||||
// Every canonical sentinel must round-trip through pack→decode.
|
||||
for &expected in &[
|
||||
MxStatus::DATA_CHANGE_OK,
|
||||
MxStatus::WRITE_COMPLETE_OK,
|
||||
MxStatus::ACTIVATE_OK,
|
||||
// SuspendPending: detail=0, success=-1, Pending=1, RequestingLmx=0.
|
||||
// → 0x81000000.
|
||||
MxStatus::SUSPEND_PENDING,
|
||||
// InvalidReferenceConfiguration: success=0, ConfigError=4,
|
||||
// RequestingLmx=0, detail=6. → 0x04000006.
|
||||
MxStatus::INVALID_REFERENCE_CONFIGURATION,
|
||||
] {
|
||||
let packed = expected.to_packed_u32();
|
||||
let round_trip = MxStatus::from_packed_u32(packed);
|
||||
assert_eq!(round_trip, expected, "round-trip failed for {expected:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_nmx_response_code_proven_mappings() {
|
||||
// Per `FUN_1010bd10:741-770` switch.
|
||||
// 0x01, 0x02 → CommunicationError + RequestingNmx
|
||||
for code in [0x01_u8, 0x02] {
|
||||
let s = MxStatus::from_nmx_response_code(code).unwrap();
|
||||
assert_eq!(s.success, 0);
|
||||
assert_eq!(s.category, MxStatusCategory::CommunicationError);
|
||||
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
|
||||
assert_eq!(s.detail, i16::from(code));
|
||||
}
|
||||
|
||||
// 0x03 → ConfigurationError + RequestingNmx
|
||||
let s = MxStatus::from_nmx_response_code(0x03).unwrap();
|
||||
assert_eq!(s.category, MxStatusCategory::ConfigurationError);
|
||||
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
|
||||
assert_eq!(s.detail, 3);
|
||||
|
||||
// 0x04 → ConfigurationError + RespondingNmx
|
||||
let s = MxStatus::from_nmx_response_code(0x04).unwrap();
|
||||
assert_eq!(s.category, MxStatusCategory::ConfigurationError);
|
||||
assert_eq!(s.detected_by, MxStatusSource::RespondingNmx);
|
||||
assert_eq!(s.detail, 4);
|
||||
|
||||
// 0x05 → CommunicationError + RespondingNmx
|
||||
let s = MxStatus::from_nmx_response_code(0x05).unwrap();
|
||||
assert_eq!(s.category, MxStatusCategory::CommunicationError);
|
||||
assert_eq!(s.detected_by, MxStatusSource::RespondingNmx);
|
||||
assert_eq!(s.detail, 5);
|
||||
|
||||
// 0x1A → CommunicationError + RequestingNmx
|
||||
let s = MxStatus::from_nmx_response_code(0x1A).unwrap();
|
||||
assert_eq!(s.category, MxStatusCategory::CommunicationError);
|
||||
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
|
||||
assert_eq!(s.detail, 0x1A);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_nmx_response_code_unmapped_returns_none() {
|
||||
// Codes outside the proven {1,2,3,4,5,0x1a} set return None — the
|
||||
// native code falls through `default` and leaves the synthesized
|
||||
// status untouched. Per `design/70-risks-and-open-questions.md`
|
||||
// R3/R4 the consumer must preserve the raw byte verbatim.
|
||||
for code in [0x00_u8, 0x06, 0x10, 0x19, 0x1B, 0x41, 0xEF, 0xFF] {
|
||||
assert!(
|
||||
MxStatus::from_nmx_response_code(code).is_none(),
|
||||
"response code 0x{code:02X} should be unmapped"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_packed_u32_normalizes_arbitrary_success_to_high_bit_only() {
|
||||
// The decoder produces `success ∈ {0, -1}`, so `to_packed_u32`
|
||||
// only checks `success != 0` — the actual integer doesn't
|
||||
// matter beyond zero/non-zero.
|
||||
let mut s = MxStatus::DATA_CHANGE_OK;
|
||||
s.success = 42; // Non-canonical value.
|
||||
let packed = s.to_packed_u32();
|
||||
assert_eq!(packed & 0x8000_0000, 0x8000_0000);
|
||||
// Round-trip normalizes to -1.
|
||||
assert_eq!(MxStatus::from_packed_u32(packed).success, -1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user