//! `INmxSvcCallback` request body parser + response body encoder. //! //! Direct port of `src/MxNativeClient/NmxSvcCallbackMessages.cs`. Decodes the //! single `byte[] buffer` parameter the AVEVA service marshals through //! `INmxSvcCallback::DataReceived` (opnum 3) and `StatusReceived` (opnum 4), //! and produces the matching `HRESULT`-bearing response body the callback //! exporter writes back. //! //! Per `NmxSvcCallbackMessages.cs:14-36`, the inbound body is: //! //! ```text //! offset size field //! 0 32 OrpcThis (encoded length without extensions) //! 32 4 size i32 LE byte-array logical length //! 36 4 max_count i32 LE conformant-array max count //! 40 size body raw bytes carried inside the callback //! ``` //! //! `size` and `max_count` are NDR-marshalled `int` values; .NET asserts both //! are non-negative and `max_count >= size` (`cs:24`). #![allow(clippy::indexing_slicing)] use crate::error::RpcError; use crate::guid::Guid; use crate::nmx_metadata::INMX_SVC_CALLBACK_IID; use crate::orpc::{OrpcThat, OrpcThis}; /// Convenience re-export so callers can match the .NET `InterfaceId` static /// (`NmxSvcCallbackMessages.cs:9`). pub const INTERFACE_ID: Guid = INMX_SVC_CALLBACK_IID; /// Opnum for `INmxSvcCallback::DataReceived` (`cs:11`). Same value as /// [`crate::nmx_metadata::DATA_RECEIVED.opnum`]. pub const DATA_RECEIVED_OPNUM: u16 = 3; /// Opnum for `INmxSvcCallback::StatusReceived` (`cs:12`). Same value as /// [`crate::nmx_metadata::STATUS_RECEIVED.opnum`]. pub const STATUS_RECEIVED_OPNUM: u16 = 4; /// Decoded callback request — mirrors `NmxCallbackRequest` /// (`NmxSvcCallbackMessages.cs:5`). #[derive(Debug, Clone, PartialEq, Eq)] pub struct NmxCallbackRequest { pub orpc_this: OrpcThis, pub body: Vec, } /// Header overhead before the `body` bytes: `OrpcThis(32) + size(4) + /// max_count(4) = 40` (`NmxSvcCallbackMessages.cs:16,29`). pub const CALLBACK_REQUEST_HEADER_LEN: usize = OrpcThis::ENCODED_LEN + 8; /// Parse an inbound callback request body. Mirrors `ParseCallbackRequest` /// (`NmxSvcCallbackMessages.cs:14-36`). /// /// # Errors /// /// - [`RpcError::ShortRead`] when `buffer.len() < 40` (`cs:16-19`). /// - [`RpcError::Decode`] when `size < 0` or `max_count < size` /// (`cs:24-27`), or when the declared `size` runs past the buffer /// (`cs:30-33`). pub fn parse_callback_request(buffer: &[u8]) -> Result { if buffer.len() < CALLBACK_REQUEST_HEADER_LEN { return Err(RpcError::ShortRead { expected: CALLBACK_REQUEST_HEADER_LEN, actual: buffer.len(), }); } let orpc_this = OrpcThis::parse(&buffer[..OrpcThis::ENCODED_LEN])?; // size and max_count are .NET `int` (i32 LE). Negative values are // explicitly rejected by the .NET reference (`cs:24`). let size_i32 = i32::from_le_bytes([ buffer[OrpcThis::ENCODED_LEN], buffer[OrpcThis::ENCODED_LEN + 1], buffer[OrpcThis::ENCODED_LEN + 2], buffer[OrpcThis::ENCODED_LEN + 3], ]); let max_count_i32 = i32::from_le_bytes([ buffer[OrpcThis::ENCODED_LEN + 4], buffer[OrpcThis::ENCODED_LEN + 5], buffer[OrpcThis::ENCODED_LEN + 6], buffer[OrpcThis::ENCODED_LEN + 7], ]); if size_i32 < 0 || max_count_i32 < size_i32 { return Err(RpcError::Decode { offset: OrpcThis::ENCODED_LEN, reason: "callback request has invalid array size metadata", buffer_len: buffer.len(), }); } let size = size_i32 as usize; let body_offset = CALLBACK_REQUEST_HEADER_LEN; if body_offset + size > buffer.len() { return Err(RpcError::Decode { offset: body_offset, reason: "callback request byte array is truncated", buffer_len: buffer.len(), }); } Ok(NmxCallbackRequest { orpc_this, body: buffer[body_offset..body_offset + size].to_vec(), }) } /// Encode the callback response body — `OrpcThat(8) + hresult(4) = 12` /// bytes. Mirrors `EncodeCallbackResponse` (`NmxSvcCallbackMessages.cs:38-44`). #[must_use] pub fn encode_callback_response(hresult: i32) -> [u8; 12] { let mut buf = [0u8; 12]; let orpc_that = OrpcThat { flags: 0, extensions_referent_id: 0, } .encode(); buf[..OrpcThat::ENCODED_LEN].copy_from_slice(&orpc_that); buf[OrpcThat::ENCODED_LEN..].copy_from_slice(&hresult.to_le_bytes()); buf } #[cfg(test)] #[allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] mod tests { use super::*; fn make_request( body: &[u8], size_override: Option, max_count_override: Option, ) -> Vec { let mut buf = Vec::with_capacity(CALLBACK_REQUEST_HEADER_LEN + body.len()); let orpc_this = OrpcThis::create(Guid::new([0x10; 16]), None).encode(); buf.extend_from_slice(&orpc_this); let size = size_override.unwrap_or(body.len() as i32); let max_count = max_count_override.unwrap_or(body.len() as i32); buf.extend_from_slice(&size.to_le_bytes()); buf.extend_from_slice(&max_count.to_le_bytes()); buf.extend_from_slice(body); buf } #[test] fn opnums_match_dotnet() { assert_eq!(DATA_RECEIVED_OPNUM, 3); assert_eq!(STATUS_RECEIVED_OPNUM, 4); } #[test] fn interface_id_matches_callback_iid() { assert_eq!(INTERFACE_ID, INMX_SVC_CALLBACK_IID); } #[test] fn parse_round_trip_empty_body() { let bytes = make_request(&[], None, None); let parsed = parse_callback_request(&bytes).unwrap(); assert!(parsed.body.is_empty()); } #[test] fn parse_round_trip_carries_payload() { let body: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02]; let bytes = make_request(body, None, None); let parsed = parse_callback_request(&bytes).unwrap(); assert_eq!(parsed.body, body); } #[test] fn parse_short_buffer_errors() { let err = parse_callback_request(&[0u8; 39]).unwrap_err(); assert!(matches!( err, RpcError::ShortRead { expected: 40, actual: 39 } )); } #[test] fn parse_negative_size_rejected() { let bytes = make_request(&[], Some(-1), Some(0)); assert!(matches!( parse_callback_request(&bytes), Err(RpcError::Decode { .. }) )); } #[test] fn parse_max_count_less_than_size_rejected() { let bytes = make_request(&[0xAA; 8], Some(8), Some(4)); assert!(matches!( parse_callback_request(&bytes), Err(RpcError::Decode { .. }) )); } #[test] fn parse_truncated_body_rejected() { // Declare 16 bytes but supply only 4. let mut bytes = make_request(&[0xAA; 4], Some(16), Some(16)); // Trim trailing bytes so the buffer is shorter than declared size. bytes.truncate(CALLBACK_REQUEST_HEADER_LEN + 4); assert!(matches!( parse_callback_request(&bytes), Err(RpcError::Decode { .. }) )); } #[test] fn encode_response_layout() { // Success response — OrpcThat zeros + hresult=0. let r = encode_callback_response(0); assert_eq!(r.len(), 12); assert_eq!(&r[..8], &[0u8; 8]); assert_eq!(&r[8..], &0i32.to_le_bytes()); // Negative hresult round-trip. let r = encode_callback_response(unchecked_negative()); assert_eq!(&r[8..], &unchecked_negative().to_le_bytes()); } fn unchecked_negative() -> i32 { // 0x80004005 = E_FAIL, the canonical generic failure HRESULT. // .NET would write `unchecked((int)0x80004005)`; Rust expresses // the same bit pattern as `i32::MIN`-aligned negative. 0x80004005u32 as i32 } }