Files
mxaccess/rust/crates/mxaccess-rpc/src/orpc.rs
T
Joseph Doherty 30138629d3 [M2] mxaccess-rpc: OXID + RemQI body codecs (wave 2)
Lands M2 wave 2 — two pure-Rust body-codec modules under
crates/mxaccess-rpc, plus a small inline ORPC framing port and a
crate-level type consolidation. Resolves F7+F8 from wave 1.

New modules
- guid.rs (4 tests) — hoisted from objref::Guid; shared by all of
  mxaccess-rpc. Resolves F7.
- error.rs — hoisted RpcError union (ShortRead, UnexpectedPacketType,
  UnknownPacketType, InvalidFragmentLength, TruncatedBindBody,
  InvalidAuthTrailer, MissingAuthValue, Decode). Resolves F8.
- orpc.rs (8 tests) — port of OrpcStructures.cs:1-141. ComVersion,
  OrpcThis (32-byte header), OrpcThat (8-byte header),
  MInterfacePointer (length-prefixed OBJREF), StdObjRef (40 bytes).
- object_exporter.rs (~530 LoC, 20 tests) — port of
  ObjectExporterMessages.cs:1-141. IObjectExporter IID, opnums,
  ResolveOxid request encoder + ResolveOxidResult/Failure parsers.
  Owned-string protocol labels cleaned up via Cow upgrade rather than
  Box::leak (ComDualStringEntry::protocol is now Cow<'static, str>).
- rem_unknown.rs (~340 LoC, 11 tests) — port of RemUnknownMessages.cs.
  IRemUnknown IID, RemQueryInterface request/response, RemQiResult.
  4-byte NDR pad in REMQIRESULT preserved as pad_after_hresult per
  CLAUDE.md unknown-bytes rule.

Test count delta: 277 -> 319 (+42; codec 215 unchanged, mxaccess-rpc
60 -> 102, codec parity 2 unchanged).
Open followups touched: F7 + F8 resolved; F9, F10, F11 added.

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

398 lines
13 KiB
Rust

//! ORPC structures shared by `IObjectExporter` and `IRemUnknown` requests.
//!
//! Direct port of `src/MxNativeClient/OrpcStructures.cs`. Provides:
//!
//! - [`ComVersion`] — 4-byte (Major u16, Minor u16) DCOM version pair.
//! - [`OrpcThis`] — 32-byte ORPC request header (`OrpcStructures.cs:10-52`).
//! - [`OrpcThat`] — 8-byte ORPC response header (`OrpcStructures.cs:54-77`).
//! - [`MInterfacePointer`] — length-prefixed OBJREF wrapper
//! (`OrpcStructures.cs:79-109`).
//! - [`StdObjRef`] — 40-byte STDOBJREF body (`OrpcStructures.cs:111-140`).
//!
//! All multi-byte fields are little-endian.
//!
//! These types are M2 wave 2 prerequisites for [`crate::object_exporter`] and
//! [`crate::rem_unknown`]; the wave 2 agents import them rather than each
//! defining their own ORPC framing.
#![allow(clippy::indexing_slicing)]
use crate::error::RpcError;
use crate::guid::Guid;
/// `OrpcStructures.cs:5-8` — DCOM version pair.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ComVersion {
pub major: u16,
pub minor: u16,
}
impl ComVersion {
/// Default version `5.7` per `OrpcStructures.cs:7`.
pub const VERSION_5_7: ComVersion = ComVersion { major: 5, minor: 7 };
pub const fn new(major: u16, minor: u16) -> Self {
Self { major, minor }
}
}
impl Default for ComVersion {
fn default() -> Self {
Self::VERSION_5_7
}
}
/// 32-byte ORPC request header (without extensions).
/// Mirrors `OrpcThis` (`OrpcStructures.cs:10-52`).
///
/// ```text
/// offset size field
/// 0 2 version.major u16 LE
/// 2 2 version.minor u16 LE
/// 4 4 flags u32 LE
/// 8 4 reserved1 u32 LE
/// 12 16 cid GUID
/// 28 4 extensions_referent_id u32 LE
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OrpcThis {
pub version: ComVersion,
pub flags: u32,
pub reserved1: u32,
pub cid: Guid,
pub extensions_referent_id: u32,
}
impl OrpcThis {
/// Encoded length without extensions — `OrpcStructures.cs:17`.
pub const ENCODED_LEN: usize = 32;
/// Construct with default version 5.7 and zeroed flags/extensions.
/// Mirrors `OrpcThis.Create(cid, version)` (`OrpcStructures.cs:19-22`).
pub fn create(cid: Guid, version: Option<ComVersion>) -> Self {
Self {
version: version.unwrap_or_default(),
flags: 0,
reserved1: 0,
cid,
extensions_referent_id: 0,
}
}
/// Decode the 32-byte header. Mirrors `OrpcThis.Parse`
/// (`OrpcStructures.cs:24-39`).
///
/// # Errors
/// Returns [`RpcError::ShortRead`] if `buffer.len() < 32`.
pub fn parse(buffer: &[u8]) -> Result<Self, RpcError> {
if buffer.len() < Self::ENCODED_LEN {
return Err(RpcError::ShortRead {
expected: Self::ENCODED_LEN,
actual: buffer.len(),
});
}
Ok(Self {
version: ComVersion::new(
u16::from_le_bytes([buffer[0], buffer[1]]),
u16::from_le_bytes([buffer[2], buffer[3]]),
),
flags: u32::from_le_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]),
reserved1: u32::from_le_bytes([buffer[8], buffer[9], buffer[10], buffer[11]]),
cid: Guid::parse(&buffer[12..28])?,
extensions_referent_id: u32::from_le_bytes([
buffer[28], buffer[29], buffer[30], buffer[31],
]),
})
}
/// Encode to 32 bytes. Mirrors `OrpcThis.Encode`
/// (`OrpcStructures.cs:41-51`).
pub fn encode(&self) -> [u8; Self::ENCODED_LEN] {
let mut buf = [0u8; Self::ENCODED_LEN];
buf[0..2].copy_from_slice(&self.version.major.to_le_bytes());
buf[2..4].copy_from_slice(&self.version.minor.to_le_bytes());
buf[4..8].copy_from_slice(&self.flags.to_le_bytes());
buf[8..12].copy_from_slice(&self.reserved1.to_le_bytes());
buf[12..28].copy_from_slice(self.cid.as_bytes());
buf[28..32].copy_from_slice(&self.extensions_referent_id.to_le_bytes());
buf
}
}
/// 8-byte ORPC response header (without extensions).
/// Mirrors `OrpcThat` (`OrpcStructures.cs:54-77`).
///
/// ```text
/// offset size field
/// 0 4 flags u32 LE
/// 4 4 extensions_referent_id u32 LE
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct OrpcThat {
pub flags: u32,
pub extensions_referent_id: u32,
}
impl OrpcThat {
/// Encoded length without extensions — `OrpcStructures.cs:56`.
pub const ENCODED_LEN: usize = 8;
/// Decode 8 bytes. Mirrors `OrpcThat.Parse` (`OrpcStructures.cs:58-68`).
///
/// # Errors
/// Returns [`RpcError::ShortRead`] if `buffer.len() < 8`.
pub fn parse(buffer: &[u8]) -> Result<Self, RpcError> {
if buffer.len() < Self::ENCODED_LEN {
return Err(RpcError::ShortRead {
expected: Self::ENCODED_LEN,
actual: buffer.len(),
});
}
Ok(Self {
flags: u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]),
extensions_referent_id: u32::from_le_bytes([
buffer[4], buffer[5], buffer[6], buffer[7],
]),
})
}
/// Encode to 8 bytes. Mirrors `OrpcThat.Encode`
/// (`OrpcStructures.cs:70-76`).
pub fn encode(&self) -> [u8; Self::ENCODED_LEN] {
let mut buf = [0u8; Self::ENCODED_LEN];
buf[0..4].copy_from_slice(&self.flags.to_le_bytes());
buf[4..8].copy_from_slice(&self.extensions_referent_id.to_le_bytes());
buf
}
}
/// Length-prefixed OBJREF byte wrapper used to carry interface pointers in
/// ORPC bodies. Mirrors `MInterfacePointer` (`OrpcStructures.cs:79-109`).
///
/// Wire layout: `u32 LE size || size bytes of OBJREF`. The Rust port owns the
/// `objref_bytes` `Vec<u8>` (matching the .NET `byte[] ObjRefBytes`).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MInterfacePointer {
pub objref_bytes: Vec<u8>,
}
impl MInterfacePointer {
/// Header length (the `u32` size prefix).
pub const SIZE_PREFIX_LEN: usize = 4;
pub fn new(objref_bytes: Vec<u8>) -> Self {
Self { objref_bytes }
}
/// Encode as `size_le32 || objref_bytes`. Mirrors `Encode`
/// (`OrpcStructures.cs:81-87`).
pub fn encode(&self) -> Vec<u8> {
let len = self.objref_bytes.len();
let mut buf = Vec::with_capacity(Self::SIZE_PREFIX_LEN + len);
let len_u32: u32 = len.try_into().unwrap_or(u32::MAX);
buf.extend_from_slice(&len_u32.to_le_bytes());
buf.extend_from_slice(&self.objref_bytes);
buf
}
/// Parse `size_le32 || size bytes` into an owned `MInterfacePointer`.
/// Mirrors `Parse` (`OrpcStructures.cs:89-103`).
///
/// # Errors
/// Returns [`RpcError::ShortRead`] if the buffer is shorter than the
/// 4-byte size prefix, or [`RpcError::Decode`] if the declared size
/// runs past the buffer.
pub fn parse(buffer: &[u8]) -> Result<Self, RpcError> {
if buffer.len() < Self::SIZE_PREFIX_LEN {
return Err(RpcError::ShortRead {
expected: Self::SIZE_PREFIX_LEN,
actual: buffer.len(),
});
}
let size = u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]) as usize;
if size > buffer.len() - Self::SIZE_PREFIX_LEN {
return Err(RpcError::Decode {
offset: Self::SIZE_PREFIX_LEN,
reason: "MInterfacePointer OBJREF payload truncated",
buffer_len: buffer.len(),
});
}
Ok(Self {
objref_bytes: buffer[Self::SIZE_PREFIX_LEN..Self::SIZE_PREFIX_LEN + size].to_vec(),
})
}
/// Parse the inner OBJREF bytes through [`crate::objref::ComObjRef::parse`].
/// Mirrors `MInterfacePointer.ParseObjRef` (`OrpcStructures.cs:105-108`).
pub fn parse_objref(&self) -> Result<crate::objref::ComObjRef, RpcError> {
crate::objref::ComObjRef::parse(&self.objref_bytes)
}
}
/// 40-byte STDOBJREF body. Mirrors `StdObjRef` (`OrpcStructures.cs:111-140`).
///
/// ```text
/// offset size field
/// 0 4 flags u32 LE
/// 4 4 public_refs u32 LE
/// 8 8 oxid u64 LE
/// 16 8 oid u64 LE
/// 24 16 ipid GUID
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct StdObjRef {
pub flags: u32,
pub public_refs: u32,
pub oxid: u64,
pub oid: u64,
pub ipid: Guid,
}
impl StdObjRef {
/// Encoded length — `OrpcStructures.cs:113`.
pub const ENCODED_LEN: usize = 40;
/// Decode 40 bytes. Mirrors `StdObjRef.Parse`
/// (`OrpcStructures.cs:115-128`).
///
/// # Errors
/// Returns [`RpcError::ShortRead`] if `buffer.len() < 40`.
pub fn parse(buffer: &[u8]) -> Result<Self, RpcError> {
if buffer.len() < Self::ENCODED_LEN {
return Err(RpcError::ShortRead {
expected: Self::ENCODED_LEN,
actual: buffer.len(),
});
}
Ok(Self {
flags: u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]),
public_refs: u32::from_le_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]),
oxid: u64::from_le_bytes([
buffer[8], buffer[9], buffer[10], buffer[11], buffer[12], buffer[13], buffer[14],
buffer[15],
]),
oid: u64::from_le_bytes([
buffer[16], buffer[17], buffer[18], buffer[19], buffer[20], buffer[21], buffer[22],
buffer[23],
]),
ipid: Guid::parse(&buffer[24..40])?,
})
}
/// Encode to 40 bytes. Mirrors `StdObjRef.Encode`
/// (`OrpcStructures.cs:130-139`).
pub fn encode(&self) -> [u8; Self::ENCODED_LEN] {
let mut buf = [0u8; Self::ENCODED_LEN];
buf[0..4].copy_from_slice(&self.flags.to_le_bytes());
buf[4..8].copy_from_slice(&self.public_refs.to_le_bytes());
buf[8..16].copy_from_slice(&self.oxid.to_le_bytes());
buf[16..24].copy_from_slice(&self.oid.to_le_bytes());
buf[24..40].copy_from_slice(self.ipid.as_bytes());
buf
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
mod tests {
use super::*;
fn sample_guid(seed: u8) -> Guid {
let mut b = [0u8; 16];
for (i, slot) in b.iter_mut().enumerate() {
*slot = seed.wrapping_add(i as u8);
}
Guid::new(b)
}
#[test]
fn com_version_default_is_5_7() {
assert_eq!(ComVersion::default(), ComVersion::new(5, 7));
}
#[test]
fn orpc_this_round_trip() {
let cid = sample_guid(0x10);
let original = OrpcThis::create(cid, None);
let encoded = original.encode();
assert_eq!(encoded.len(), OrpcThis::ENCODED_LEN);
let decoded = OrpcThis::parse(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn orpc_this_short_buffer_errors() {
assert!(matches!(
OrpcThis::parse(&[0u8; 31]),
Err(RpcError::ShortRead {
expected: 32,
actual: 31
})
));
}
#[test]
fn orpc_that_round_trip() {
let original = OrpcThat {
flags: 0xDEAD_BEEF,
extensions_referent_id: 0x1234_5678,
};
let encoded = original.encode();
assert_eq!(encoded.len(), OrpcThat::ENCODED_LEN);
let decoded = OrpcThat::parse(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn m_interface_pointer_round_trip() {
let mip = MInterfacePointer::new(vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE]);
let encoded = mip.encode();
assert_eq!(encoded.len(), 4 + 5);
// Size prefix is 5.
assert_eq!(&encoded[0..4], &5u32.to_le_bytes());
let decoded = MInterfacePointer::parse(&encoded).unwrap();
assert_eq!(decoded, mip);
}
#[test]
fn m_interface_pointer_truncated_payload_errors() {
// Declares 16 bytes but only supplies 4 after the prefix.
let mut bad = Vec::new();
bad.extend_from_slice(&16u32.to_le_bytes());
bad.extend_from_slice(&[0u8; 4]);
let err = MInterfacePointer::parse(&bad).unwrap_err();
assert!(matches!(err, RpcError::Decode { .. }));
}
#[test]
fn std_objref_round_trip() {
let original = StdObjRef {
flags: 0,
public_refs: 5,
oxid: 0x1122_3344_5566_7788,
oid: 0x99AA_BBCC_DDEE_FF00,
ipid: sample_guid(0x55),
};
let encoded = original.encode();
assert_eq!(encoded.len(), StdObjRef::ENCODED_LEN);
let decoded = StdObjRef::parse(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn std_objref_short_buffer_errors() {
assert!(matches!(
StdObjRef::parse(&[0u8; 39]),
Err(RpcError::ShortRead {
expected: 40,
actual: 39
})
));
}
}