//! `NmxOperationStatusMessage` — completion / status-word frames. //! //! Direct port of `src/MxNativeCodec/NmxOperationStatusMessage.cs`. //! //! Two on-the-wire shapes are recognised by the inner-body parser: //! //! 1. **5-byte status-word frame** — `00 00 SS SS CC` where `SS SS` is a u16 //! LE status code and `CC` is a completion code. The single proven mapping //! is `00 00 50 80 00` → [`MxStatus::WRITE_COMPLETE_OK`] //! (`NmxOperationStatusMessage.cs:48-62`, //! `design/40-protocol-invariants.md:346`). //! 2. **1-byte completion-only frame** — a single byte `CC`. Three values are //! observed in the wild (`0x00`, `0x41`, `0xEF`) but the byte→status //! mapping is unproven; they are preserved verbatim per //! `design/70-risks-and-open-questions.md` R3/R4 and //! `NmxOperationStatusMessage.cs:36-46,69-76`. //! //! The .NET reference also exposes `TryParseProcessDataReceivedBody`, which //! peels an outer `NmxObservedEnvelope` before delegating to the inner-body //! parser. The outer envelope codec has not yet been ported to Rust; only //! [`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 /// (`NmxOperationStatusMessage.cs:3-7`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum NmxOperationStatusFormat { /// Single-byte completion frame (`NmxOperationStatusMessage.cs:5,36-46`). CompletionOnly, /// 5-byte `00 00 SS SS CC` status-word frame /// (`NmxOperationStatusMessage.cs:6,48-62`). StatusWord, } /// Decoded operation-status frame /// (`NmxOperationStatusMessage.cs:9-15` — record fields). /// /// The four payload fields preserve the raw on-wire bytes; [`Self::status`] /// carries the promoted [`MxStatus`] when a known mapping exists, or an /// unpromoted placeholder otherwise (see `CompletionOnly` and the fallback /// branch of `StatusWord`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct NmxOperationStatusMessage { /// Which inner frame shape was observed. pub format: NmxOperationStatusFormat, /// First byte of the 5-byte frame (`NmxOperationStatusMessage.cs:54`). /// `0` for `CompletionOnly` frames. pub command: u8, /// `inner[2..4]` u16 LE for `StatusWord` (`NmxOperationStatusMessage.cs:50`). /// `0` for `CompletionOnly` frames. pub status_code: u16, /// Completion byte. `inner[0]` for `CompletionOnly` /// (`NmxOperationStatusMessage.cs:38`); `inner[4]` for `StatusWord` /// (`NmxOperationStatusMessage.cs:51`). pub completion_code: u8, /// Promoted status. The only proven promotion is /// `status_code == 0x8050 && completion_code == 0x00 → WRITE_COMPLETE_OK` /// (`NmxOperationStatusMessage.cs:57`, /// `design/40-protocol-invariants.md:346`). Every other shape is wrapped /// in an `Unknown`/`Unknown` placeholder with the raw byte preserved in /// `detail`. pub status: MxStatus, } impl NmxOperationStatusMessage { /// `true` for the proven `00 00 50 80 00` frame /// (`NmxOperationStatusMessage.cs:16-18`). pub fn is_mx_access_write_complete(&self) -> bool { self.format == NmxOperationStatusFormat::StatusWord && self.status_code == 0x8050 && 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 { 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`). /// /// Mirrors `NmxOperationStatusMessage.TryParseInner` /// (`NmxOperationStatusMessage.cs:34-67`). /// /// # Errors /// /// Returns [`CodecError::ShortRead`] when the buffer length matches no /// recognised shape. The .NET reference returns `false` and a `null!` /// out-param; the Rust port surfaces the failure as a typed error so /// callers can distinguish "not an operation-status frame" from /// "successfully parsed". Match on the error to mirror the bool API. pub fn try_parse_inner(inner: &[u8]) -> Result { if inner.len() == 1 { // CompletionOnly — `NmxOperationStatusMessage.cs:36-46`. let completion_code = inner[0]; return Ok(Self { format: NmxOperationStatusFormat::CompletionOnly, command: 0, status_code: 0, completion_code, status: create_unpromoted_completion_status(completion_code), }); } if inner.len() == 5 && inner[0] == 0x00 && inner[1] == 0x00 { // StatusWord — `NmxOperationStatusMessage.cs:48-62`. let status_code = u16::from(inner[2]) | (u16::from(inner[3]) << 8); let completion_code = inner[4]; // Only the (0x8050, 0x00) shape is promoted to a typed status. // Every other (status_code, completion_code) pair is preserved as // an Unknown/Unknown placeholder with the raw byte in `detail`, // mirroring `NmxOperationStatusMessage.cs:57-61`. // // The .NET fallback packs `detail` as: // completion_code == 0x00 ? (short)status_code : completion_code // We replicate the same selection here, including the // `unchecked((short)statusCode)` reinterpretation (i.e. the u16's // bit pattern as i16). let status = if status_code == 0x8050 && completion_code == 0x00 { MxStatus::WRITE_COMPLETE_OK } else { let detail = if completion_code == 0x00 { // Reinterpret the u16 status_code as i16 (two's complement). status_code as i16 } else { i16::from(completion_code) }; MxStatus { success: 0, category: MxStatusCategory::Unknown, detected_by: MxStatusSource::Unknown, detail, } }; return Ok(Self { format: NmxOperationStatusFormat::StatusWord, command: inner[0], status_code, completion_code, status, }); } Err(CodecError::ShortRead { // 1 or 5 are the two valid lengths; report the smaller for the // diagnostic. Callers that need the strict bool API should pattern // match on `Err(_) => false`. expected: 1, actual: inner.len(), }) } } /// Build the unpromoted placeholder status used by `CompletionOnly` frames /// (`NmxOperationStatusMessage.cs:69-76`). fn create_unpromoted_completion_status(completion_code: u8) -> MxStatus { MxStatus { success: 0, category: MxStatusCategory::Unknown, detected_by: MxStatusSource::Unknown, detail: i16::from(completion_code), } } #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)] mod tests { use super::*; #[test] fn write_complete_ok_frame() { // The proven 5-byte mapping (`design/40-protocol-invariants.md:346`). let frame = [0x00, 0x00, 0x50, 0x80, 0x00]; let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap(); assert_eq!(msg.format, NmxOperationStatusFormat::StatusWord); assert_eq!(msg.command, 0x00); assert_eq!(msg.status_code, 0x8050); assert_eq!(msg.completion_code, 0x00); assert_eq!(msg.status, MxStatus::WRITE_COMPLETE_OK); assert!(msg.is_mx_access_write_complete()); } #[test] fn status_word_unknown_with_completion_zero_packs_status_code_as_i16() { // status_code 0x8051, completion 0x00 — not the proven mapping; falls // through to the unpromoted branch with detail = (i16)0x8051 = -32687. // Mirrors `NmxOperationStatusMessage.cs:61` (`unchecked((short)statusCode)`). let frame = [0x00, 0x00, 0x51, 0x80, 0x00]; let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap(); assert_eq!(msg.format, NmxOperationStatusFormat::StatusWord); assert_eq!(msg.status_code, 0x8051); assert_eq!(msg.completion_code, 0x00); assert_eq!(msg.status.category, MxStatusCategory::Unknown); assert_eq!(msg.status.detected_by, MxStatusSource::Unknown); assert_eq!(msg.status.detail, 0x8051u16 as i16); assert!(!msg.is_mx_access_write_complete()); } #[test] fn status_word_unknown_with_nonzero_completion_packs_completion_in_detail() { // completion_code != 0 — detail = completion_code as i16 // (`NmxOperationStatusMessage.cs:61`). let frame = [0x00, 0x00, 0x50, 0x80, 0x42]; let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap(); assert_eq!(msg.completion_code, 0x42); assert_eq!(msg.status.detail, 0x42); assert_eq!(msg.status.category, MxStatusCategory::Unknown); assert!(!msg.is_mx_access_write_complete()); } #[test] fn completion_only_zero_byte() { // 1-byte 0x00 — preserved verbatim per design R3/R4. let frame = [0x00]; let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap(); assert_eq!(msg.format, NmxOperationStatusFormat::CompletionOnly); assert_eq!(msg.command, 0); assert_eq!(msg.status_code, 0); assert_eq!(msg.completion_code, 0x00); assert_eq!(msg.status.detail, 0x00); assert_eq!(msg.status.category, MxStatusCategory::Unknown); // `CompletionOnly` is never promoted to WriteCompleteOk. assert!(!msg.is_mx_access_write_complete()); } #[test] fn completion_only_0x41_byte() { // 1-byte 0x41 — observed in the wild, mapping unproven // (`design/70-risks-and-open-questions.md` R4). let frame = [0x41]; let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap(); assert_eq!(msg.format, NmxOperationStatusFormat::CompletionOnly); assert_eq!(msg.completion_code, 0x41); assert_eq!(msg.status.detail, 0x41); assert_eq!(msg.status.category, MxStatusCategory::Unknown); } #[test] fn completion_only_0xef_byte() { // 1-byte 0xEF — observed in the wild, mapping unproven // (`design/70-risks-and-open-questions.md` R4). let frame = [0xEF]; let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap(); assert_eq!(msg.format, NmxOperationStatusFormat::CompletionOnly); assert_eq!(msg.completion_code, 0xEF); // 0xEF as i16 is 0x00EF (zero-extended), not -17. assert_eq!(msg.status.detail, 0xEF); } #[test] fn rejects_unknown_length() { // 0 / 2 / 3 / 4 / 6 bytes — all non-recognised shapes. for len in [0_usize, 2, 3, 4, 6, 16] { let buf = vec![0u8; len]; assert!( NmxOperationStatusMessage::try_parse_inner(&buf).is_err(), "length {len} should be rejected" ); } } #[test] fn rejects_5_byte_frame_without_leading_zeros() { // 5 bytes with non-zero leading bytes — not a StatusWord frame // (`NmxOperationStatusMessage.cs:48` requires `inner[0] == 0 && inner[1] == 0`). let frame = [0x01, 0x00, 0x50, 0x80, 0x00]; assert!(NmxOperationStatusMessage::try_parse_inner(&frame).is_err()); let frame = [0x00, 0x01, 0x50, 0x80, 0x00]; assert!(NmxOperationStatusMessage::try_parse_inner(&frame).is_err()); } #[test] fn status_code_is_little_endian() { // `inner[2..4]` is read as u16 LE — `inner[2] | (inner[3] << 8)`. // 0xAA at [2], 0xBB at [3] → 0xBBAA. let frame = [0x00, 0x00, 0xAA, 0xBB, 0x00]; 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); } }