[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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user