30138629d3
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>
398 lines
13 KiB
Rust
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
|
|
})
|
|
));
|
|
}
|
|
}
|