ecfcc3f429
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>
546 lines
20 KiB
Rust
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
|
|
})
|
|
));
|
|
}
|
|
}
|