[M2] mxaccess-rpc: NMX metadata + callback messages + OBJREF builder
Lands the codec-only prerequisites for M2 wave 3 (callback exporter). The TCP server itself (port of ManagedCallbackExporter.cs's TcpListener + accept loop) follows next iteration in the mxaccess-callback crate. New modules - nmx_metadata.rs (9 tests) — port of NmxProcedureMetadata.cs. INmxService2 + INmxSvcCallback IIDs, NdrProcedureDescriptor with per-opnum metadata for the 9 INmxService2 procedures (opnums 3..11) and 2 INmxSvcCallback procedures (opnums 3, 4). - nmx_callback_messages.rs (8 tests) — port of NmxSvcCallbackMessages.cs. parse_callback_request decodes OrpcThis + i32 size + i32 max_count + body bytes; encode_callback_response produces the 12-byte OrpcThat + HRESULT response. objref.rs additions - ComObjRefBuilder::create_standard_objref (8 tests) — port of the second class in ManagedCallbackExporter.cs:337-393. Pure-Rust OBJREF emitter that builds 68-byte header + dual-string array. Note this is *not* the Win32 CoMarshalInterface-based ComObjRefProvider.cs (still open as F6); it's the higher-level emitter the callback exporter uses to build OBJREF bytes from primitives. - CALLBACK_OBJREF_AUTH_SERVICES const exposes the 7-entry auth-service tower-id table (NTLM SSP through Kerberos extension) the .NET reference advertises in every callback OBJREF. Test count delta: 319 -> 344 (+25; mxaccess-rpc 102 -> 127, codec unchanged at 215, parity unchanged at 2). All four DoD gates green. Open followups touched: none new; F6 advances toward resolution but the windows-rs Win32 wrapper part stays open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
|
||||
pub mod error;
|
||||
pub mod guid;
|
||||
pub mod nmx_callback_messages;
|
||||
pub mod nmx_metadata;
|
||||
pub mod ntlm;
|
||||
pub mod object_exporter;
|
||||
pub mod objref;
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
//! `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<u8>,
|
||||
}
|
||||
|
||||
/// 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<NmxCallbackRequest, RpcError> {
|
||||
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<i32>,
|
||||
max_count_override: Option<i32>,
|
||||
) -> Vec<u8> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! NMX procedure metadata.
|
||||
//!
|
||||
//! Direct port of `src/MxNativeClient/NmxProcedureMetadata.cs`. Defines the
|
||||
//! `INmxService2` and `INmxSvcCallback` interface IIDs and the per-opnum NDR
|
||||
//! procedure descriptors used by the .NET reference's `ManagedCallbackExporter`
|
||||
//! and `ManagedNmxService2Client`.
|
||||
//!
|
||||
//! These values are wire-load-bearing for M2 wave 3 (callback exporter) and
|
||||
//! M3 (NMX session). Each IID is also enforced by the COM `[Guid(...)]`
|
||||
//! attributes on the matching interfaces in `NmxComContracts.cs:7,52,84`.
|
||||
|
||||
use crate::guid::Guid;
|
||||
|
||||
/// `INmxService2` IID `2630A513-A974-4B1A-8025-457A9A7C56B8`
|
||||
/// (`NmxProcedureMetadata.cs:5`, `NmxComContracts.cs:51`).
|
||||
pub const INMX_SERVICE2_IID: Guid = Guid::new([
|
||||
0x13, 0xA5, 0x30, 0x26, 0x74, 0xA9, 0x1A, 0x4B, 0x80, 0x25, 0x45, 0x7A, 0x9A, 0x7C, 0x56, 0xB8,
|
||||
]);
|
||||
|
||||
/// `INmxSvcCallback` IID `B49F92F7-C748-4169-8ECA-A0670B012746`
|
||||
/// (`NmxProcedureMetadata.cs:6`, `NmxComContracts.cs:84`).
|
||||
pub const INMX_SVC_CALLBACK_IID: Guid = Guid::new([
|
||||
0xF7, 0x92, 0x9F, 0xB4, 0x48, 0xC7, 0x69, 0x41, 0x8E, 0xCA, 0xA0, 0x67, 0x0B, 0x01, 0x27, 0x46,
|
||||
]);
|
||||
|
||||
/// NDR procedure descriptor — mirrors `NdrProcedureDescriptor`
|
||||
/// (`NmxProcedureMetadata.cs:108-115`). Captures the opnum + the x86 stack
|
||||
/// size and client/server buffer sizes the LMX MIDL stub publishes via
|
||||
/// `NMIDL_PROC_INFO`. The Rust port carries these for parity with the .NET
|
||||
/// reference; a future M3 NMX client may use them to size pre-allocated
|
||||
/// buffers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct NdrProcedureDescriptor {
|
||||
pub interface_id: Guid,
|
||||
pub name: &'static str,
|
||||
pub opnum: u16,
|
||||
pub x86_stack_size: u16,
|
||||
pub client_buffer_size: u16,
|
||||
pub server_buffer_size: u16,
|
||||
pub parameter_count_including_return: u8,
|
||||
}
|
||||
|
||||
impl NdrProcedureDescriptor {
|
||||
pub const fn new(
|
||||
interface_id: Guid,
|
||||
name: &'static str,
|
||||
opnum: u16,
|
||||
x86_stack_size: u16,
|
||||
client_buffer_size: u16,
|
||||
server_buffer_size: u16,
|
||||
parameter_count_including_return: u8,
|
||||
) -> Self {
|
||||
Self {
|
||||
interface_id,
|
||||
name,
|
||||
opnum,
|
||||
x86_stack_size,
|
||||
client_buffer_size,
|
||||
server_buffer_size,
|
||||
parameter_count_including_return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- INmxService2 procedures (`NmxProcedureMetadata.cs:8-87`) -----------
|
||||
|
||||
/// `INmxService2::RegisterEngine` — opnum 3 (`cs:8-15`).
|
||||
pub const REGISTER_ENGINE: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SERVICE2_IID, "RegisterEngine", 3, 20, 8, 8, 4);
|
||||
|
||||
/// `INmxService2::UnRegisterEngine` — opnum 4 (`cs:17-24`).
|
||||
pub const UNREGISTER_ENGINE: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SERVICE2_IID, "UnRegisterEngine", 4, 12, 8, 8, 2);
|
||||
|
||||
/// `INmxService2::Connect` — opnum 5 (`cs:26-33`).
|
||||
pub const CONNECT: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SERVICE2_IID, "Connect", 5, 24, 32, 8, 5);
|
||||
|
||||
/// `INmxService2::TransferData` — opnum 6 (`cs:35-42`).
|
||||
pub const TRANSFER_DATA: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SERVICE2_IID, "TransferData", 6, 28, 32, 8, 6);
|
||||
|
||||
/// `INmxService2::AddSubscriberEngine` — opnum 7 (`cs:44-51`).
|
||||
pub const ADD_SUBSCRIBER_ENGINE: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SERVICE2_IID, "AddSubscriberEngine", 7, 24, 32, 8, 5);
|
||||
|
||||
/// `INmxService2::RemoveSubscriberEngine` — opnum 8 (`cs:53-60`).
|
||||
pub const REMOVE_SUBSCRIBER_ENGINE: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SERVICE2_IID, "RemoveSubscriberEngine", 8, 24, 32, 8, 5);
|
||||
|
||||
/// `INmxService2::SetHeartbeatSendInterval` — opnum 9 (`cs:62-69`).
|
||||
pub const SET_HEARTBEAT_SEND_INTERVAL: NdrProcedureDescriptor = NdrProcedureDescriptor::new(
|
||||
INMX_SERVICE2_IID,
|
||||
"SetHeartbeatSendInterval",
|
||||
9,
|
||||
16,
|
||||
16,
|
||||
8,
|
||||
3,
|
||||
);
|
||||
|
||||
/// `INmxService2::RegisterEngine2` — opnum 10 (`cs:71-78`).
|
||||
pub const REGISTER_ENGINE_2: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SERVICE2_IID, "RegisterEngine2", 10, 24, 16, 8, 5);
|
||||
|
||||
/// `INmxService2::GetPartnerVersion` — opnum 11 (`cs:80-87`).
|
||||
pub const GET_PARTNER_VERSION: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SERVICE2_IID, "GetPartnerVersion", 11, 24, 24, 36, 5);
|
||||
|
||||
// --- INmxSvcCallback procedures (`NmxProcedureMetadata.cs:89-105`) -------
|
||||
|
||||
/// `INmxSvcCallback::DataReceived` — opnum 3 (`cs:89-96`).
|
||||
pub const DATA_RECEIVED: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SVC_CALLBACK_IID, "DataReceived", 3, 16, 8, 8, 3);
|
||||
|
||||
/// `INmxSvcCallback::StatusReceived` — opnum 4 (`cs:98-105`).
|
||||
pub const STATUS_RECEIVED: NdrProcedureDescriptor =
|
||||
NdrProcedureDescriptor::new(INMX_SVC_CALLBACK_IID, "StatusReceived", 4, 16, 8, 8, 3);
|
||||
|
||||
/// All `INmxService2` procedures in opnum order. Convenience for callers
|
||||
/// that want to iterate the table.
|
||||
pub const INMX_SERVICE2_PROCEDURES: &[NdrProcedureDescriptor] = &[
|
||||
REGISTER_ENGINE,
|
||||
UNREGISTER_ENGINE,
|
||||
CONNECT,
|
||||
TRANSFER_DATA,
|
||||
ADD_SUBSCRIBER_ENGINE,
|
||||
REMOVE_SUBSCRIBER_ENGINE,
|
||||
SET_HEARTBEAT_SEND_INTERVAL,
|
||||
REGISTER_ENGINE_2,
|
||||
GET_PARTNER_VERSION,
|
||||
];
|
||||
|
||||
/// All `INmxSvcCallback` procedures in opnum order.
|
||||
pub const INMX_SVC_CALLBACK_PROCEDURES: &[NdrProcedureDescriptor] =
|
||||
&[DATA_RECEIVED, STATUS_RECEIVED];
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::panic
|
||||
)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn inmx_service2_iid_matches_dotnet_d_format() {
|
||||
// .NET `new Guid("2630A513-A974-4B1A-8025-457A9A7C56B8").ToString("D")`
|
||||
assert_eq!(
|
||||
INMX_SERVICE2_IID.to_string(),
|
||||
"2630a513-a974-4b1a-8025-457a9a7c56b8"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inmx_svc_callback_iid_matches_dotnet_d_format() {
|
||||
// The exact IID re-asserted in the OBJREF capture
|
||||
// `captures/057-managed-callback-route-service-trace-saved/probe.stdout.txt:6`
|
||||
// (objref bytes 8..24).
|
||||
assert_eq!(
|
||||
INMX_SVC_CALLBACK_IID.to_string(),
|
||||
"b49f92f7-c748-4169-8eca-a0670b012746"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inmx_service2_opnums_are_3_through_11() {
|
||||
let opnums: Vec<u16> = INMX_SERVICE2_PROCEDURES.iter().map(|p| p.opnum).collect();
|
||||
assert_eq!(opnums, vec![3, 4, 5, 6, 7, 8, 9, 10, 11]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inmx_svc_callback_opnums_are_3_and_4() {
|
||||
let opnums: Vec<u16> = INMX_SVC_CALLBACK_PROCEDURES
|
||||
.iter()
|
||||
.map(|p| p.opnum)
|
||||
.collect();
|
||||
assert_eq!(opnums, vec![3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn procedure_names_match_dotnet_nameof() {
|
||||
// .NET uses `nameof(...)` so names match the C# method identifier.
|
||||
assert_eq!(REGISTER_ENGINE.name, "RegisterEngine");
|
||||
assert_eq!(REGISTER_ENGINE_2.name, "RegisterEngine2");
|
||||
assert_eq!(GET_PARTNER_VERSION.name, "GetPartnerVersion");
|
||||
assert_eq!(STATUS_RECEIVED.name, "StatusReceived");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_engine_2_metadata() {
|
||||
// Spot-check the parameters most likely to be load-bearing for M3:
|
||||
// opnum 10, 24-byte x86 stack, 16-byte client buffer, 5 params
|
||||
// including the HRESULT return (`cs:71-78`).
|
||||
assert_eq!(REGISTER_ENGINE_2.opnum, 10);
|
||||
assert_eq!(REGISTER_ENGINE_2.x86_stack_size, 24);
|
||||
assert_eq!(REGISTER_ENGINE_2.client_buffer_size, 16);
|
||||
assert_eq!(REGISTER_ENGINE_2.server_buffer_size, 8);
|
||||
assert_eq!(REGISTER_ENGINE_2.parameter_count_including_return, 5);
|
||||
assert_eq!(REGISTER_ENGINE_2.interface_id, INMX_SERVICE2_IID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_data_is_largest_x86_stack() {
|
||||
// TransferData (opnum 6) has the largest x86 stack at 28 bytes
|
||||
// because it carries the `ref byte messageBody` payload pointer.
|
||||
let max = INMX_SERVICE2_PROCEDURES
|
||||
.iter()
|
||||
.map(|p| p.x86_stack_size)
|
||||
.max()
|
||||
.unwrap();
|
||||
assert_eq!(max, TRANSFER_DATA.x86_stack_size);
|
||||
assert_eq!(TRANSFER_DATA.opnum, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_procedures_use_callback_iid() {
|
||||
for p in INMX_SVC_CALLBACK_PROCEDURES {
|
||||
assert_eq!(p.interface_id, INMX_SVC_CALLBACK_IID);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service2_procedures_use_service2_iid() {
|
||||
for p in INMX_SERVICE2_PROCEDURES {
|
||||
assert_eq!(p.interface_id, INMX_SERVICE2_IID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,6 +318,149 @@ fn read_u64_le(bytes: &[u8], offset: usize) -> u64 {
|
||||
const _: () = assert!(OBJREF_HEADER_LEN == 68);
|
||||
const _: () = assert!(OBJREF_SIGNATURE == 0x574F_454D);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ComObjRefBuilder — pure-Rust OBJREF emitter.
|
||||
// Direct port of the second class in
|
||||
// `src/MxNativeClient/ManagedCallbackExporter.cs:337-393`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Auth-service tower IDs the .NET reference advertises in every callback
|
||||
/// OBJREF. Mirrors the hard-coded array at
|
||||
/// `ManagedCallbackExporter.cs:362`. Each id appears in the security-binding
|
||||
/// portion of the dual-string array followed by `0xFFFF` and a terminator.
|
||||
///
|
||||
/// IDs in order: NTLM SSP (0x0009), GSS Negotiate (0x001E), Kerberos (0x0010),
|
||||
/// SSL/TLS (0x000A), Schannel (0x0016), DPA (0x001F), Kerberos extension
|
||||
/// (0x000E). The Rust port carries the same set verbatim — no synthesis.
|
||||
pub const CALLBACK_OBJREF_AUTH_SERVICES: [u16; 7] =
|
||||
[0x0009, 0x001E, 0x0010, 0x000A, 0x0016, 0x001F, 0x000E];
|
||||
|
||||
/// Builds standard OBJREF byte buffers for the callback exporter to publish.
|
||||
///
|
||||
/// Mirrors the static `ComObjRefBuilder` class
|
||||
/// (`src/MxNativeClient/ManagedCallbackExporter.cs:337-393`). The .NET reference
|
||||
/// only ever emits *standard* OBJREFs (`flags = 1`); the Rust port matches.
|
||||
///
|
||||
/// This is the higher-level emitter that builds OBJREF bytes from primitives.
|
||||
/// It is **not** the Win32 `CoMarshalInterface`-based emitter from
|
||||
/// `ComObjRefProvider.cs` — that wrapper around `ole32` is still tracked as
|
||||
/// open follow-up F6 (it requires `windows-rs` and the M2 wave 3 callback
|
||||
/// exporter to register the emitted OBJREF with COM).
|
||||
pub struct ComObjRefBuilder;
|
||||
|
||||
impl ComObjRefBuilder {
|
||||
/// Build a standard-OBJREF buffer for a given IID, OXID/OID/IPID, and one
|
||||
/// or more `ncacn_ip_tcp` string bindings (e.g. `"hostname[5985]"`).
|
||||
/// Mirrors `ComObjRefBuilder.CreateStandardObjRef`
|
||||
/// (`ManagedCallbackExporter.cs:339-392`).
|
||||
///
|
||||
/// # Layout (`cs:348-389`)
|
||||
///
|
||||
/// ```text
|
||||
/// offset size field
|
||||
/// 0 4 signature u32 LE = 0x574F454D ("MEOW")
|
||||
/// 4 4 flags u32 LE = 1
|
||||
/// 8 16 iid GUID
|
||||
/// 24 4 std_flags u32 LE
|
||||
/// 28 4 public_refs u32 LE
|
||||
/// 32 8 oxid u64 LE
|
||||
/// 40 8 oid u64 LE
|
||||
/// 48 16 ipid GUID
|
||||
/// 64 2 entries u16 LE (count of u16 code units below)
|
||||
/// 66 2 security_offset u16 LE (in u16 code units)
|
||||
/// 68 .. dual-string array (variable-length u16 LE words)
|
||||
/// ```
|
||||
///
|
||||
/// # `entries` and `security_offset`
|
||||
///
|
||||
/// `entries` is the **total u16-code-unit count** of the dual-string
|
||||
/// array (string bindings + 0 separator + 7 security entries + final 0).
|
||||
/// `security_offset` is the index (in u16 units) where security bindings
|
||||
/// begin — `cs:348` computes this as
|
||||
/// `sum(1 + binding.len() + 1 for binding in stringBindings) + 1`, i.e.
|
||||
/// per-binding `tower_id` (1 word) + `binding.len()` ASCII chars (one
|
||||
/// word each) + null terminator (1 word), plus the trailing 0 separator
|
||||
/// that ends the string section.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Never panics. All length math saturates: bindings longer than
|
||||
/// `u16::MAX - HEADER_LEN/2 - SECURITY_TAIL_LEN` are not representable
|
||||
/// in the 16-bit `entries` field, and the .NET reference does not guard
|
||||
/// against this either; callers are expected to keep bindings short
|
||||
/// (typical `hostname[port]` is < 100 chars).
|
||||
#[must_use]
|
||||
pub fn create_standard_objref(
|
||||
iid: Guid,
|
||||
std_flags: u32,
|
||||
public_refs: u32,
|
||||
oxid: u64,
|
||||
oid: u64,
|
||||
ipid: Guid,
|
||||
string_bindings: &[&str],
|
||||
) -> Vec<u8> {
|
||||
// security_offset = sum_{b in string_bindings}(1 + b.len() + 1) + 1
|
||||
// (cs:348). u16-truncating cast mirrors `(ushort)`.
|
||||
let security_offset: u16 = string_bindings
|
||||
.iter()
|
||||
.map(|b| 1 + b.len() + 1)
|
||||
.sum::<usize>()
|
||||
.saturating_add(1)
|
||||
.min(u16::MAX as usize) as u16;
|
||||
|
||||
// Build the u16 word array.
|
||||
let mut words: Vec<u16> = Vec::new();
|
||||
|
||||
// String-bindings section: per binding, [0x0007 (ncacn_ip_tcp), each
|
||||
// ASCII char as u16, terminator 0] (cs:350-359).
|
||||
for binding in string_bindings {
|
||||
words.push(0x0007);
|
||||
for ch in binding.chars() {
|
||||
words.push(ch as u16);
|
||||
}
|
||||
words.push(0);
|
||||
}
|
||||
// 0 separator that ends the string section (cs:361).
|
||||
words.push(0);
|
||||
|
||||
// Security-bindings section: 7 hard-coded tower entries, each
|
||||
// [tower_id, 0xFFFF, 0] (cs:362-367).
|
||||
for &auth in &CALLBACK_OBJREF_AUTH_SERVICES {
|
||||
words.push(auth);
|
||||
words.push(0xFFFF);
|
||||
words.push(0);
|
||||
}
|
||||
|
||||
// Final terminator (cs:369).
|
||||
words.push(0);
|
||||
|
||||
// u16-truncating cast mirrors `(ushort)words.Count` (cs:371).
|
||||
let entries: u16 = words.len().min(u16::MAX as usize) as u16;
|
||||
let mut buffer = vec![0u8; OBJREF_HEADER_LEN + words.len() * 2];
|
||||
|
||||
// Fixed 68-byte header (cs:373-382).
|
||||
buffer[0..4].copy_from_slice(&OBJREF_SIGNATURE.to_le_bytes());
|
||||
buffer[4..8].copy_from_slice(&1u32.to_le_bytes()); // flags = 1 (OBJREF_STANDARD)
|
||||
buffer[8..24].copy_from_slice(iid.as_bytes());
|
||||
buffer[24..28].copy_from_slice(&std_flags.to_le_bytes());
|
||||
buffer[28..32].copy_from_slice(&public_refs.to_le_bytes());
|
||||
buffer[32..40].copy_from_slice(&oxid.to_le_bytes());
|
||||
buffer[40..48].copy_from_slice(&oid.to_le_bytes());
|
||||
buffer[48..64].copy_from_slice(ipid.as_bytes());
|
||||
buffer[64..66].copy_from_slice(&entries.to_le_bytes());
|
||||
buffer[66..68].copy_from_slice(&security_offset.to_le_bytes());
|
||||
|
||||
// Dual-string array body (cs:384-389).
|
||||
let mut offset = OBJREF_HEADER_LEN;
|
||||
for word in &words {
|
||||
buffer[offset..offset + 2].copy_from_slice(&word.to_le_bytes());
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(
|
||||
clippy::unwrap_used,
|
||||
@@ -626,4 +769,147 @@ mod tests {
|
||||
assert_eq!(OBJREF_HEADER_LEN, 68);
|
||||
assert_eq!(OBJREF_SIGNATURE, 0x574F_454D);
|
||||
}
|
||||
|
||||
// ---- ComObjRefBuilder tests --------------------------------------
|
||||
|
||||
#[test]
|
||||
fn builder_emits_meow_header_and_flags() {
|
||||
let iid = Guid::new([0xAA; 16]);
|
||||
let ipid = Guid::new([0xBB; 16]);
|
||||
let buf = ComObjRefBuilder::create_standard_objref(
|
||||
iid,
|
||||
0x280,
|
||||
5,
|
||||
0x0123_4567_89AB_CDEF,
|
||||
0xFEDC_BA98_7654_3210,
|
||||
ipid,
|
||||
&["host[5985]"],
|
||||
);
|
||||
assert!(buf.len() >= OBJREF_HEADER_LEN);
|
||||
// Signature
|
||||
assert_eq!(&buf[0..4], &OBJREF_SIGNATURE.to_le_bytes());
|
||||
// flags = 1 (OBJREF_STANDARD)
|
||||
assert_eq!(&buf[4..8], &1u32.to_le_bytes());
|
||||
// IID
|
||||
assert_eq!(&buf[8..24], iid.as_bytes());
|
||||
// std_flags
|
||||
assert_eq!(&buf[24..28], &0x280u32.to_le_bytes());
|
||||
// public_refs
|
||||
assert_eq!(&buf[28..32], &5u32.to_le_bytes());
|
||||
// OXID/OID
|
||||
assert_eq!(&buf[32..40], &0x0123_4567_89AB_CDEFu64.to_le_bytes());
|
||||
assert_eq!(&buf[40..48], &0xFEDC_BA98_7654_3210u64.to_le_bytes());
|
||||
// IPID
|
||||
assert_eq!(&buf[48..64], ipid.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_round_trips_through_parser() {
|
||||
// The emitted OBJREF must parse back through ComObjRef::parse with
|
||||
// the same key fields.
|
||||
let iid = Guid::new([0x11; 16]);
|
||||
let ipid = Guid::new([0x22; 16]);
|
||||
let buf = ComObjRefBuilder::create_standard_objref(
|
||||
iid,
|
||||
0x280,
|
||||
5,
|
||||
0x1111_2222_3333_4444,
|
||||
0x5555_6666_7777_8888,
|
||||
ipid,
|
||||
&["DESKTOP[12345]"],
|
||||
);
|
||||
|
||||
let parsed = ComObjRef::parse(&buf).unwrap();
|
||||
assert_eq!(parsed.signature, OBJREF_SIGNATURE);
|
||||
assert_eq!(parsed.flags, 1);
|
||||
assert_eq!(parsed.iid, iid);
|
||||
assert_eq!(parsed.standard_flags, 0x280);
|
||||
assert_eq!(parsed.public_refs, 5);
|
||||
assert_eq!(parsed.oxid, 0x1111_2222_3333_4444);
|
||||
assert_eq!(parsed.oid, 0x5555_6666_7777_8888);
|
||||
assert_eq!(parsed.ipid, ipid);
|
||||
|
||||
// First decoded entry should be the ncacn_ip_tcp string binding,
|
||||
// and at least one security binding (auth-service tail) follows.
|
||||
let first = &parsed.dual_string_entries_decoded[0];
|
||||
assert_eq!(first.tower_id, 0x0007);
|
||||
assert_eq!(first.protocol, "ncacn_ip_tcp");
|
||||
assert_eq!(first.value, "DESKTOP[12345]");
|
||||
assert!(!first.is_security_binding);
|
||||
|
||||
let security_count = parsed
|
||||
.dual_string_entries_decoded
|
||||
.iter()
|
||||
.filter(|e| e.is_security_binding)
|
||||
.count();
|
||||
assert!(
|
||||
security_count >= 1,
|
||||
"expected at least one security binding, got {security_count}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_security_offset_matches_dotnet_formula() {
|
||||
// security_offset = sum(1 + binding.len() + 1) + 1 (cs:348).
|
||||
// For one binding "host[12]" (8 chars): 1 + 8 + 1 + 1 = 11.
|
||||
let buf = ComObjRefBuilder::create_standard_objref(
|
||||
Guid::ZERO,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
Guid::ZERO,
|
||||
&["host[12]"],
|
||||
);
|
||||
let security_offset = u16::from_le_bytes([buf[66], buf[67]]);
|
||||
assert_eq!(security_offset, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_two_bindings_security_offset() {
|
||||
// Two bindings: "a[1]" (4) + "b[2]" (4):
|
||||
// (1+4+1) + (1+4+1) + 1 = 13.
|
||||
let buf = ComObjRefBuilder::create_standard_objref(
|
||||
Guid::ZERO,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
Guid::ZERO,
|
||||
&["a[1]", "b[2]"],
|
||||
);
|
||||
let security_offset = u16::from_le_bytes([buf[66], buf[67]]);
|
||||
assert_eq!(security_offset, 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_emits_seven_security_entries() {
|
||||
// Each security entry contributes 3 u16 words [tower_id, 0xFFFF, 0].
|
||||
// Total security words = 7 * 3 = 21, plus a trailing 0 = 22.
|
||||
// String section for one binding "h[1]" (4 chars): 1+4+1+1 = 7 words.
|
||||
// Total entries = 7 + 22 = 29.
|
||||
let buf =
|
||||
ComObjRefBuilder::create_standard_objref(Guid::ZERO, 0, 5, 0, 0, Guid::ZERO, &["h[1]"]);
|
||||
let entries = u16::from_le_bytes([buf[64], buf[65]]);
|
||||
assert_eq!(entries, 29);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_auth_services_table_matches_dotnet_order() {
|
||||
// The auth-service tower ids in the security tail must appear in the
|
||||
// order the .NET reference writes them (cs:362).
|
||||
assert_eq!(
|
||||
CALLBACK_OBJREF_AUTH_SERVICES,
|
||||
[0x0009, 0x001E, 0x0010, 0x000A, 0x0016, 0x001F, 0x000E]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_total_buffer_length_matches_words_count() {
|
||||
// entries * 2 + HEADER_LEN
|
||||
let buf =
|
||||
ComObjRefBuilder::create_standard_objref(Guid::ZERO, 0, 5, 0, 0, Guid::ZERO, &["x[1]"]);
|
||||
let entries = u16::from_le_bytes([buf[64], buf[65]]) as usize;
|
||||
assert_eq!(buf.len(), OBJREF_HEADER_LEN + entries * 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user