[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>
This commit is contained in:
Joseph Doherty
2026-05-05 07:14:29 -04:00
parent 95bd218183
commit 30138629d3
9 changed files with 1894 additions and 142 deletions
+20 -63
View File
@@ -26,7 +26,10 @@
use std::fmt::Write as _;
use thiserror::Error;
// `Guid` and `RpcError` are crate-shared since M2 wave 2 — see
// `design/followups.md` F7+F8.
pub use crate::error::RpcError;
pub use crate::guid::Guid;
/// Encoded layout per `ComObjRef.cs:25-39`:
///
@@ -64,65 +67,6 @@ const IPID_OFFSET: usize = 48;
const DUAL_STRING_ENTRIES_OFFSET: usize = 64;
const DUAL_STRING_SECURITY_OFFSET_OFFSET: usize = 66;
/// 16-byte GUID. Stored as little-endian wire bytes for the first three groups
/// (Data1 u32 LE, Data2 u16 LE, Data3 u16 LE) followed by 8 big-endian
/// `Data4` bytes — matches the byte layout produced by .NET
/// `new Guid(ReadOnlySpan<byte>)` (`ComObjRef.cs:31,36`).
///
/// Kept as a self-contained type to avoid pulling `uuid` into `mxaccess-rpc`;
/// the sibling DCE/RPC PDU codec may consolidate to a shared type at the
/// loop-driver level.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Guid(pub [u8; 16]);
impl Guid {
pub const fn new(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
impl std::fmt::Display for Guid {
/// Mirrors .NET `Guid.ToString("D")`: dashed hex, lowercase, e.g.
/// `b49f92f7-c748-4169-8eca-a0670b012746`. The first three groups are
/// little-endian on the wire so are byte-swapped on display.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let b = &self.0;
write!(
f,
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
b[3],
b[2],
b[1],
b[0],
b[5],
b[4],
b[7],
b[6],
b[8],
b[9],
b[10],
b[11],
b[12],
b[13],
b[14],
b[15],
)
}
}
/// Errors produced by the OBJREF parser.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RpcError {
/// Buffer too short to satisfy a fixed-layout read.
#[error("short read: expected {expected} bytes, got {actual}")]
ShortRead { expected: usize, actual: usize },
}
/// One decoded entry of the OBJREF dual-string array. `value` is the
/// printable-ASCII escaping of the UTF-16 string per `ComObjRef.cs:82-91` —
/// non-printable code units appear as `<XXXX>` lowercase hex. `is_security_binding`
@@ -130,10 +74,17 @@ pub enum RpcError {
/// `DualStringSecurityOffset`.
///
/// Mirrors `ComDualStringEntry` (`ComObjRef.cs:138-145`).
///
/// `protocol` is `Cow<'static, str>` because the OBJREF parser uses the
/// 7-entry static table (`Cow::Borrowed`) while the M2 wave 2 OXID-resolve
/// parser uses `format!("protseq_0x{:04x}", tower_id)` (`Cow::Owned`) for
/// unknown tower ids (`ObjectExporterMessages.cs:120`). The two parsers
/// share the entry type but emit different protocol labels for the same
/// tower id — this is intentional and matches the .NET reference.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComDualStringEntry {
pub tower_id: u16,
pub protocol: &'static str,
pub protocol: std::borrow::Cow<'static, str>,
pub value: String,
pub is_security_binding: bool,
}
@@ -310,7 +261,7 @@ fn decode_dual_string_array(
strings.push(ComDualStringEntry {
tower_id,
protocol: protocol_tower_name(tower_id),
protocol: std::borrow::Cow::Borrowed(protocol_tower_name(tower_id)),
value: text,
is_security_binding: entry_start >= security_offset as usize,
});
@@ -368,7 +319,12 @@ const _: () = assert!(OBJREF_HEADER_LEN == 68);
const _: () = assert!(OBJREF_SIGNATURE == 0x574F_454D);
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
mod tests {
use super::*;
@@ -474,6 +430,7 @@ mod tests {
assert_eq!(expected, 68);
assert_eq!(actual, 67);
}
other => panic!("expected ShortRead, got {other:?}"),
}
}