//! `INmxService2` request/response codecs. //! //! Direct port of `src/MxNativeClient/NmxService2Messages.cs`. Provides //! pure-codec encoders/decoders for the 9 procedures the .NET reference //! marshals against `INmxService2` (opnums 3..11) plus the small set of //! NDR helpers used by `RegisterEngine2` (`EncodeBstrUserMarshal`, //! `EncodeNullInterfacePointer`, `EncodeInterfacePointer`). //! //! All wire fields are little-endian. Each encoder returns a `Vec` //! that the transport (`crate::transport::DceRpcTcpClient::call_bound`) //! sends as the `stub_data` of a `Request` PDU. #![allow(clippy::indexing_slicing)] use crate::error::RpcError; use crate::nmx_metadata::INMX_SERVICE2_IID; use crate::orpc::{OrpcThat, OrpcThis}; /// `NmxServiceClass` CLSID `AE24BD51-2E80-44CC-905B-E5446C942BEB` /// (`NmxService2Messages.cs:12`, also `NmxComContracts.cs:7`). pub const NMX_SERVICE_CLSID: crate::guid::Guid = crate::guid::Guid::new([ 0x51, 0xBD, 0x24, 0xAE, 0x80, 0x2E, 0xCC, 0x44, 0x90, 0x5B, 0xE5, 0x44, 0x6C, 0x94, 0x2B, 0xEB, ]); /// `INmxService2` IID — re-exported for convenience /// (`NmxService2Messages.cs:13`). pub const INTERFACE_ID: crate::guid::Guid = INMX_SERVICE2_IID; // --- Opnums (`NmxService2Messages.cs:15-23`) ---------------------------- pub const REGISTER_ENGINE_OPNUM: u16 = 3; pub const UNREGISTER_ENGINE_OPNUM: u16 = 4; pub const CONNECT_OPNUM: u16 = 5; pub const TRANSFER_DATA_OPNUM: u16 = 6; pub const ADD_SUBSCRIBER_ENGINE_OPNUM: u16 = 7; pub const REMOVE_SUBSCRIBER_ENGINE_OPNUM: u16 = 8; pub const SET_HEARTBEAT_SEND_INTERVAL_OPNUM: u16 = 9; pub const REGISTER_ENGINE_2_OPNUM: u16 = 10; pub const GET_PARTNER_VERSION_OPNUM: u16 = 11; // --- Records ------------------------------------------------------------ /// Decoded `GetPartnerVersion` response — mirrors /// `NmxGetPartnerVersionResult` (`cs:6`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct NmxGetPartnerVersionResult { pub orpc_that: OrpcThat, pub partner_version: i32, pub hresult: i32, } /// Decoded HRESULT-only response (Connect / Register* / Unregister / /// Set / TransferData / Add/Remove subscriber). Mirrors /// `NmxHResultResponse` (`cs:8`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct NmxHResultResponse { pub orpc_that: OrpcThat, pub hresult: i32, } // --- Encoders ----------------------------------------------------------- /// `GetPartnerVersion` request (`cs:25-37`). /// /// Layout: `OrpcThis(32) || galaxy_id(4) || platform_id(4) || engine_id(4)`. #[must_use] pub fn encode_get_partner_version_request( orpc_this: OrpcThis, galaxy_id: i32, platform_id: i32, engine_id: i32, ) -> Vec { let mut buf = vec![0u8; OrpcThis::ENCODED_LEN + 12]; buf[..OrpcThis::ENCODED_LEN].copy_from_slice(&orpc_this.encode()); buf[32..36].copy_from_slice(&galaxy_id.to_le_bytes()); buf[36..40].copy_from_slice(&platform_id.to_le_bytes()); buf[40..44].copy_from_slice(&engine_id.to_le_bytes()); buf } /// `Connect` request (`cs:52-66`). /// /// Layout: `OrpcThis(32) || local_engine_id(4) || remote_galaxy_id(4) || /// remote_platform_id(4) || remote_engine_id(4)`. #[must_use] pub fn encode_connect_request( orpc_this: OrpcThis, local_engine_id: i32, remote_galaxy_id: i32, remote_platform_id: i32, remote_engine_id: i32, ) -> Vec { let mut buf = vec![0u8; OrpcThis::ENCODED_LEN + 16]; buf[..OrpcThis::ENCODED_LEN].copy_from_slice(&orpc_this.encode()); buf[32..36].copy_from_slice(&local_engine_id.to_le_bytes()); buf[36..40].copy_from_slice(&remote_galaxy_id.to_le_bytes()); buf[40..44].copy_from_slice(&remote_platform_id.to_le_bytes()); buf[44..48].copy_from_slice(&remote_engine_id.to_le_bytes()); buf } /// `AddSubscriberEngine` / `RemoveSubscriberEngine` request shape /// (`cs:68-82`). Both opnums share this layout — the .NET reference /// reuses the same encoder. #[must_use] pub fn encode_subscriber_engine_request( orpc_this: OrpcThis, local_engine_id: i32, subscriber_galaxy_id: i32, subscriber_platform_id: i32, subscriber_engine_id: i32, ) -> Vec { let mut buf = vec![0u8; OrpcThis::ENCODED_LEN + 16]; buf[..OrpcThis::ENCODED_LEN].copy_from_slice(&orpc_this.encode()); buf[32..36].copy_from_slice(&local_engine_id.to_le_bytes()); buf[36..40].copy_from_slice(&subscriber_galaxy_id.to_le_bytes()); buf[40..44].copy_from_slice(&subscriber_platform_id.to_le_bytes()); buf[44..48].copy_from_slice(&subscriber_engine_id.to_le_bytes()); buf } /// `UnRegisterEngine` request (`cs:84-92`). /// /// Layout: `OrpcThis(32) || local_engine_id(4)`. #[must_use] pub fn encode_unregister_engine_request(orpc_this: OrpcThis, local_engine_id: i32) -> Vec { let mut buf = vec![0u8; OrpcThis::ENCODED_LEN + 4]; buf[..OrpcThis::ENCODED_LEN].copy_from_slice(&orpc_this.encode()); buf[32..36].copy_from_slice(&local_engine_id.to_le_bytes()); buf } /// `SetHeartbeatSendInterval` request (`cs:94-104`). /// /// Layout: `OrpcThis(32) || ticks_per_beat(4) || max_missed_ticks(4)`. #[must_use] pub fn encode_set_heartbeat_send_interval_request( orpc_this: OrpcThis, ticks_per_beat: i32, max_missed_ticks: i32, ) -> Vec { let mut buf = vec![0u8; OrpcThis::ENCODED_LEN + 8]; buf[..OrpcThis::ENCODED_LEN].copy_from_slice(&orpc_this.encode()); buf[32..36].copy_from_slice(&ticks_per_beat.to_le_bytes()); buf[36..40].copy_from_slice(&max_missed_ticks.to_le_bytes()); buf } /// `TransferData` request (`cs:106-124`). /// /// Layout (NDR-aligned to 4 bytes overall): /// /// ```text /// offset size field /// 0 32 OrpcThis /// 32 4 remote_galaxy_id i32 LE /// 36 4 remote_platform_id i32 LE /// 40 4 remote_engine_id i32 LE /// 44 4 message_length i32 LE /// 48 4 max_count i32 LE = message_length /// 52..(52+len) len message_body /// (padded to 4-byte alignment) /// ``` #[must_use] pub fn encode_transfer_data_request( orpc_this: OrpcThis, remote_galaxy_id: i32, remote_platform_id: i32, remote_engine_id: i32, message_body: &[u8], ) -> Vec { let body_offset = OrpcThis::ENCODED_LEN + 20; let padded_length = align_up(body_offset + message_body.len(), 4); let mut buf = vec![0u8; padded_length]; buf[..OrpcThis::ENCODED_LEN].copy_from_slice(&orpc_this.encode()); buf[32..36].copy_from_slice(&remote_galaxy_id.to_le_bytes()); buf[36..40].copy_from_slice(&remote_platform_id.to_le_bytes()); buf[40..44].copy_from_slice(&remote_engine_id.to_le_bytes()); let body_len = i32::try_from(message_body.len()).unwrap_or(i32::MAX); buf[44..48].copy_from_slice(&body_len.to_le_bytes()); buf[48..52].copy_from_slice(&body_len.to_le_bytes()); buf[body_offset..body_offset + message_body.len()].copy_from_slice(message_body); buf } /// `RegisterEngine2` request (`cs:126-154`). /// /// Layout (each section 4-byte NDR-aligned): /// /// ```text /// 0 32 OrpcThis /// 32 4 local_engine_id i32 LE /// 36 4 domain_marker i32 LE = 0x72657355 ("User" little-endian) /// 40 var bstr (12-byte BSTR header + UTF-16 chars, no NUL) /// (aligned to 4) 4 version i32 LE /// (followed by the InterfacePointer structure for the callback OBJREF) /// ``` /// /// `domain_marker = 0x72657355` is `"Useu"`-style ASCII reversed; the /// .NET reference writes it verbatim at `cs:146` and the LMX server /// parses it back as a string-form domain identity. The Rust port does /// not interpret it; it round-trips the constant per CLAUDE.md /// "preserve unknown bytes" rule. /// /// When `callback_obj_ref` is `None` the encoder writes a 4-byte null /// interface pointer (`cs:134-135`); when `Some(bytes)`, it wraps the /// OBJREF in a 12-byte InterfacePointer header per `cs:206-215`. #[must_use] pub fn encode_register_engine_2_request( orpc_this: OrpcThis, local_engine_id: i32, engine_name: &str, version: i32, callback_obj_ref: Option<&[u8]>, ) -> Vec { let bstr = encode_bstr_user_marshal(engine_name); let callback = match callback_obj_ref { None => encode_null_interface_pointer().to_vec(), Some(bytes) => encode_interface_pointer(bytes), }; let bstr_offset = OrpcThis::ENCODED_LEN + 8; let version_offset = align_up(bstr_offset + bstr.len(), 4); let length = align_up(version_offset + 4 + callback.len(), 4); let mut buf = vec![0u8; length]; buf[..OrpcThis::ENCODED_LEN].copy_from_slice(&orpc_this.encode()); buf[32..36].copy_from_slice(&local_engine_id.to_le_bytes()); // "Useu" domain marker — `cs:146`. buf[36..40].copy_from_slice(&0x7265_5355i32.to_le_bytes()); buf[40..40 + bstr.len()].copy_from_slice(&bstr); buf[version_offset..version_offset + 4].copy_from_slice(&version.to_le_bytes()); let cb_off = version_offset + 4; buf[cb_off..cb_off + callback.len()].copy_from_slice(&callback); buf } /// Encode a UTF-16LE BSTR as the LMX MIDL stub expects /// (`NmxService2Messages.cs:156-171`): /// /// ```text /// 0..4 char_count i32 LE number of UTF-16 code units (no NUL) /// 4..8 byte_count i32 LE 2 * char_count /// 8..12 char_count i32 LE repeated (NDR conformant array max count) /// 12.. UTF-16LE chars (no terminator) /// ``` #[must_use] pub fn encode_bstr_user_marshal(value: &str) -> Vec { let utf16: Vec = value.encode_utf16().collect(); let char_count = i32::try_from(utf16.len()).unwrap_or(i32::MAX); let byte_count = i32::try_from(utf16.len() * 2).unwrap_or(i32::MAX); let mut buf = vec![0u8; 12 + utf16.len() * 2]; buf[0..4].copy_from_slice(&char_count.to_le_bytes()); buf[4..8].copy_from_slice(&byte_count.to_le_bytes()); buf[8..12].copy_from_slice(&char_count.to_le_bytes()); for (i, ch) in utf16.iter().enumerate() { buf[12 + i * 2..12 + i * 2 + 2].copy_from_slice(&ch.to_le_bytes()); } buf } /// 4-byte null interface pointer — `cs:201-204`. The LMX server treats /// a 4-byte zero referent as "no callback registered". #[must_use] pub const fn encode_null_interface_pointer() -> [u8; 4] { [0, 0, 0, 0] } /// Wrap an OBJREF in the InterfacePointer NDR layout — `cs:206-215`: /// /// ```text /// 0..4 referent_id u32 LE = 0x00020000 /// 4..8 length i32 LE = obj_ref.len() /// 8..12 max_count i32 LE = obj_ref.len() /// 12.. obj_ref bytes (padded to 4-byte alignment) /// ``` #[must_use] pub fn encode_interface_pointer(obj_ref: &[u8]) -> Vec { let length = align_up(12 + obj_ref.len(), 4); let mut buf = vec![0u8; length]; buf[0..4].copy_from_slice(&0x0002_0000u32.to_le_bytes()); let len_i32 = i32::try_from(obj_ref.len()).unwrap_or(i32::MAX); buf[4..8].copy_from_slice(&len_i32.to_le_bytes()); buf[8..12].copy_from_slice(&len_i32.to_le_bytes()); buf[12..12 + obj_ref.len()].copy_from_slice(obj_ref); buf } // --- Decoders ----------------------------------------------------------- /// Parse a `GetPartnerVersion` response (`cs:39-50`). /// /// # Errors /// [`RpcError::ShortRead`] if the buffer is shorter than 16 bytes. pub fn parse_get_partner_version_response( buffer: &[u8], ) -> Result { let need = OrpcThat::ENCODED_LEN + 8; if buffer.len() < need { return Err(RpcError::ShortRead { expected: need, actual: buffer.len(), }); } let orpc_that = OrpcThat::parse(&buffer[..OrpcThat::ENCODED_LEN])?; Ok(NmxGetPartnerVersionResult { orpc_that, partner_version: i32::from_le_bytes([buffer[8], buffer[9], buffer[10], buffer[11]]), hresult: i32::from_le_bytes([buffer[12], buffer[13], buffer[14], buffer[15]]), }) } /// Parse a generic HRESULT response (`cs:173-183`). /// /// # Errors /// [`RpcError::ShortRead`] if the buffer is shorter than 12 bytes. pub fn parse_hresult_response(buffer: &[u8]) -> Result { let need = OrpcThat::ENCODED_LEN + 4; if buffer.len() < need { return Err(RpcError::ShortRead { expected: need, actual: buffer.len(), }); } let orpc_that = OrpcThat::parse(&buffer[..OrpcThat::ENCODED_LEN])?; Ok(NmxHResultResponse { orpc_that, hresult: i32::from_le_bytes([buffer[8], buffer[9], buffer[10], buffer[11]]), }) } const fn align_up(value: usize, alignment: usize) -> usize { let r = value % alignment; if r == 0 { value } else { value + alignment - r } } #[cfg(test)] #[allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] mod tests { use super::*; use crate::guid::Guid; fn sample_orpc_this() -> OrpcThis { OrpcThis::create(Guid::new([0xAB; 16]), None) } #[test] fn nmx_service_clsid_matches_dotnet_d_format() { // .NET `new Guid("AE24BD51-2E80-44CC-905B-E5446C942BEB").ToString("D")`. assert_eq!( NMX_SERVICE_CLSID.to_string(), "ae24bd51-2e80-44cc-905b-e5446c942beb" ); } #[test] fn opnum_constants_match_dotnet() { assert_eq!(REGISTER_ENGINE_OPNUM, 3); assert_eq!(UNREGISTER_ENGINE_OPNUM, 4); assert_eq!(CONNECT_OPNUM, 5); assert_eq!(TRANSFER_DATA_OPNUM, 6); assert_eq!(ADD_SUBSCRIBER_ENGINE_OPNUM, 7); assert_eq!(REMOVE_SUBSCRIBER_ENGINE_OPNUM, 8); assert_eq!(SET_HEARTBEAT_SEND_INTERVAL_OPNUM, 9); assert_eq!(REGISTER_ENGINE_2_OPNUM, 10); assert_eq!(GET_PARTNER_VERSION_OPNUM, 11); } #[test] fn get_partner_version_request_layout() { let buf = encode_get_partner_version_request(sample_orpc_this(), 1, 2, 3); // 32 (OrpcThis) + 12 = 44. assert_eq!(buf.len(), 44); assert_eq!(&buf[32..36], &1i32.to_le_bytes()); assert_eq!(&buf[36..40], &2i32.to_le_bytes()); assert_eq!(&buf[40..44], &3i32.to_le_bytes()); } #[test] fn connect_request_layout() { let buf = encode_connect_request(sample_orpc_this(), 10, 11, 12, 13); assert_eq!(buf.len(), 48); assert_eq!(&buf[32..36], &10i32.to_le_bytes()); assert_eq!(&buf[44..48], &13i32.to_le_bytes()); } #[test] fn subscriber_engine_request_layout() { let buf = encode_subscriber_engine_request(sample_orpc_this(), 1, 2, 3, 4); assert_eq!(buf.len(), 48); assert_eq!(&buf[44..48], &4i32.to_le_bytes()); } #[test] fn unregister_engine_request_layout() { let buf = encode_unregister_engine_request(sample_orpc_this(), 0xCAFE); assert_eq!(buf.len(), 36); assert_eq!(&buf[32..36], &0xCAFEi32.to_le_bytes()); } #[test] fn set_heartbeat_send_interval_request_layout() { let buf = encode_set_heartbeat_send_interval_request(sample_orpc_this(), 100, 5); assert_eq!(buf.len(), 40); assert_eq!(&buf[32..36], &100i32.to_le_bytes()); assert_eq!(&buf[36..40], &5i32.to_le_bytes()); } #[test] fn transfer_data_request_layout_aligned() { // body length 8 — body offset 52 + 8 = 60, already 4-aligned. let body = [0xAAu8; 8]; let buf = encode_transfer_data_request(sample_orpc_this(), 1, 2, 3, &body); assert_eq!(buf.len(), 60); assert_eq!(&buf[44..48], &8i32.to_le_bytes()); // length assert_eq!(&buf[48..52], &8i32.to_le_bytes()); // max_count assert_eq!(&buf[52..60], &body); } #[test] fn transfer_data_request_layout_padded() { // body length 5 — body offset 52 + 5 = 57, padded to 60. let body = [0xBBu8; 5]; let buf = encode_transfer_data_request(sample_orpc_this(), 1, 2, 3, &body); assert_eq!(buf.len(), 60); assert_eq!(&buf[44..48], &5i32.to_le_bytes()); assert_eq!(&buf[52..57], &body); // padding bytes 57..60 are zero (default vec! init). assert_eq!(&buf[57..60], &[0u8; 3]); } #[test] fn bstr_user_marshal_layout() { // "AB" (2 chars, 4 UTF-16LE bytes) → header 12 + 4 bytes = 16. let buf = encode_bstr_user_marshal("AB"); assert_eq!(buf.len(), 16); assert_eq!(&buf[0..4], &2i32.to_le_bytes()); assert_eq!(&buf[4..8], &4i32.to_le_bytes()); assert_eq!(&buf[8..12], &2i32.to_le_bytes()); assert_eq!(&buf[12..14], &b"A\0"[..]); assert_eq!(&buf[14..16], &b"B\0"[..]); } #[test] fn bstr_empty_string() { let buf = encode_bstr_user_marshal(""); assert_eq!(buf.len(), 12); assert_eq!(&buf[0..4], &0i32.to_le_bytes()); assert_eq!(&buf[4..8], &0i32.to_le_bytes()); assert_eq!(&buf[8..12], &0i32.to_le_bytes()); } #[test] fn null_interface_pointer_is_4_zero_bytes() { assert_eq!(encode_null_interface_pointer(), [0u8; 4]); } #[test] fn interface_pointer_referent_id_and_aligned_length() { // OBJREF length 6 → 12 + 6 = 18 → align 4 → 20. let obj = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06]; let buf = encode_interface_pointer(&obj); assert_eq!(buf.len(), 20); assert_eq!(&buf[0..4], &0x0002_0000u32.to_le_bytes()); assert_eq!(&buf[4..8], &6i32.to_le_bytes()); assert_eq!(&buf[8..12], &6i32.to_le_bytes()); assert_eq!(&buf[12..18], &obj); assert_eq!(&buf[18..20], &[0u8; 2]); // padding } #[test] fn register_engine_2_request_with_callback_objref() { // Uses a 16-byte OBJREF stub. let obj = [0xCCu8; 16]; let buf = encode_register_engine_2_request(sample_orpc_this(), 42, "Engine", 6, Some(&obj)); // OrpcThis(32) + local_engine(4) + marker(4) + bstr(24) + version(4) + callback(28+pad) // bstr: 12 + 12 (6 UTF-16 chars) = 24 // callback: 12 + 16 = 28, already 4-aligned // Total = 32 + 4 + 4 + 24 + 4 + 28 = 96. assert_eq!(buf.len(), 96); assert_eq!(&buf[32..36], &42i32.to_le_bytes()); assert_eq!(&buf[36..40], &0x7265_5355i32.to_le_bytes()); // "Useu" // BSTR header at 40..52. assert_eq!(&buf[40..44], &6i32.to_le_bytes()); // 6 chars // version at 64. assert_eq!(&buf[64..68], &6i32.to_le_bytes()); } #[test] fn register_engine_2_request_with_null_callback() { let buf = encode_register_engine_2_request(sample_orpc_this(), 7, "X", 1, None); // OrpcThis(32) + 4 + 4 + bstr(14 → align 16) + version(4) + callback(4) // bstr: 12 + 2 = 14 → align to 16 // callback: 4 (null), version_offset + 4 + 4 = ?. Let's just check total > 0. assert!(buf.len() >= 32 + 4 + 4 + 14 + 4 + 4); // The null interface-pointer slot is 4 bytes of zero at the end. let len = buf.len(); assert_eq!(&buf[len - 4..len], &[0u8; 4]); } #[test] fn parse_get_partner_version_response_happy_path() { let mut buf = vec![0u8; 16]; // OrpcThat at 0..8 (zeros). buf[8..12].copy_from_slice(&6i32.to_le_bytes()); // partner_version buf[12..16].copy_from_slice(&0i32.to_le_bytes()); // S_OK let r = parse_get_partner_version_response(&buf).unwrap(); assert_eq!(r.partner_version, 6); assert_eq!(r.hresult, 0); } #[test] fn parse_get_partner_version_short_buffer_errors() { assert!(matches!( parse_get_partner_version_response(&[0u8; 15]), Err(RpcError::ShortRead { expected: 16, actual: 15 }) )); } #[test] fn parse_hresult_response_happy_path() { let mut buf = vec![0u8; 12]; buf[8..12].copy_from_slice(&0x8000_4005u32.to_le_bytes()); // E_FAIL let r = parse_hresult_response(&buf).unwrap(); assert_eq!(r.hresult, 0x8000_4005u32 as i32); } #[test] fn parse_hresult_response_short_buffer_errors() { assert!(matches!( parse_hresult_response(&[0u8; 11]), Err(RpcError::ShortRead { expected: 12, actual: 11 }) )); } }