Files
mxaccess/rust/crates/mxaccess-rpc/src/nmx_service2_messages.rs
T
Joseph Doherty ecfcc3f429 [M3] mxaccess-rpc: NmxService2 codec + F9 ResolveOxid wrappers
Two units of work in one commit:

1. nmx_service2_messages.rs (~470 LoC, 18 tests) — port of
   NmxService2Messages.cs. Encoders for all 9 INmxService2 opnums
   (RegisterEngine, UnRegisterEngine, Connect, TransferData,
   AddSubscriberEngine, RemoveSubscriberEngine, SetHeartbeatSendInterval,
   RegisterEngine2, GetPartnerVersion) plus BSTR + InterfacePointer NDR
   helpers used by RegisterEngine2 marshalling. Decoders for the
   GetPartnerVersion result and the generic HRESULT response. M3 stream
   B (NmxClient) will be a thin layer over these + the transport.

2. object_exporter_client.rs (~290 LoC, 6 tests including 2 real-socket
   tokio tests) — resolves followup F9. Implements:
   - resolve_oxid_unauthenticated (cs:14-30)
   - resolve_oxid_with_managed_ntlm_packet_integrity (cs:66-81)
   ResolveOxidOutcome enum disambiguates the two response shapes the
   .NET reference parses (typed result vs 4-byte failure). The two SSPI
   flavours (cs:32-47, cs:49-64) are permanently skipped — they wrap
   .NET-only System.Net.Security.SspiClientContext.

design/followups.md: F9 moved to Resolved with this commit's hash.

Test count delta: 364 -> 389 (+25; mxaccess-rpc 137 -> 162; +18 from
nmx_service2_messages, +7 from object_exporter_client which includes
the +2 fall-through tests for the dual-shape response decoder).
Open followups touched: F9 resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:56:11 -04:00

546 lines
20 KiB
Rust

//! `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<u8>`
//! 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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
let utf16: Vec<u16> = 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<u8> {
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<NmxGetPartnerVersionResult, RpcError> {
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<NmxHResultResponse, RpcError> {
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
})
));
}
}