Files
mxaccess/rust/crates/mxaccess-rpc/src/rem_unknown.rs
T
Joseph Doherty 161b0cedfa
rust / build / test / clippy / fmt (push) Has been cancelled
[F10 + F11] mxaccess-rpc: structural ports for ResolveOxid2 + RemAddRef/RemRelease
Closes F10 and F11 per option (b) of each followup's resolve
criterion: hand-rolled body codecs derived from the [MS-DCOM]
spec, ship structurally with no live fixture (the .NET reference
doesn't call these opnums), validate against captured frames when
they become available.

F10 — IObjectExporter::ResolveOxid2 (opnum 4):
  Per [MS-DCOM] §3.1.2.5.1.4. New parse_resolve_oxid2_result
  mirrors parse_resolve_oxid_result exactly except for the extra
  4-byte COMVERSION slot (u16 major + u16 minor) between
  authn_hint and error_status. Trailing-fields check tightens
  from 24 bytes (opnum 0) to 28 bytes (opnum 4). New ComVersion +
  ResolveOxid2Result types. referent_id=0 short-circuit returns
  empty bindings + default ComVersion + tail-status, matching
  opnum 0's pattern.

F11 — IRemUnknown::RemAddRef + RemRelease (opnums 4 and 5):
  Per [MS-DCOM] §3.1.1.5.6 + §2.2.19 (REMINTERFACEREF). Both
  opnums share the wire shape, so:
    - encode_rem_add_ref_request + encode_rem_release_request
      both delegate to a shared encode_remref_array_request
      helper.
    - parse_remref_response is shared between them too — they
      have an identical OrpcThat + referent_id + max_count +
      N×HRESULT + error_code layout.
  New RemInterfaceRef struct (ipid + public_refs + private_refs,
  ENCODED_LEN = 24) + RemRefResponse decoded shape.

8 new structural tests across both modules pin every documented
edge of each shape — short stubs, referent-zero short-circuits,
truncated-trailing detection, full multi-element round-trips.
mxaccess-rpc 183 → 188 tests; default-feature clippy clean.

Both followups documented "**Status:** Awaiting wire-fixture
capture" prior to this commit; the structural-port option was
explicitly listed as resolution path (b) in each. Future captured
frames will validate the byte-by-byte match — until then the
port is byte-faithful to the spec but unverified against a live
peer (which is fine for shipping since neither opnum is on the
NMX session's hot path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 03:24:12 -04:00

758 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `IRemUnknown` request/response codecs.
//!
//! Direct port of `src/MxNativeClient/RemUnknownMessages.cs`. Provides:
//!
//! - [`IREM_UNKNOWN_IID`] — `IRemUnknown` interface IID
//! (`RemUnknownMessages.cs:7`).
//! - [`REM_QUERY_INTERFACE_OPNUM`], [`REM_ADD_REF_OPNUM`],
//! [`REM_RELEASE_OPNUM`] — DCE/RPC opnums (`RemUnknownMessages.cs:8-10`).
//! - [`encode_rem_query_interface_request`] — builds the body for the
//! `RemQueryInterface` request (`RemUnknownMessages.cs:12-33`).
//! - [`parse_rem_query_interface_response`] — decodes the response body
//! (`RemUnknownMessages.cs:35-59`).
//! - [`RemQueryInterfaceResponse`] (`RemUnknownMessages.cs:62`).
//! - [`RemQiResult`] — `REMQIRESULT` body (`RemUnknownMessages.cs:64-79`).
//!
//! All multi-byte fields are little-endian.
//!
//! The 4-byte pad in `REMQIRESULT` between `hresult` and the embedded
//! `STDOBJREF` is preserved on decode (`pad_after_hresult: [u8; 4]`) per
//! the CLAUDE.md "preserve unknown bytes" rule. The native .NET reference
//! reads-and-discards it (`RemUnknownMessages.cs:75-77`); Rust holds onto
//! the bytes so callers can round-trip captures byte-for-byte.
#![allow(clippy::indexing_slicing)]
use crate::error::RpcError;
use crate::guid::Guid;
use crate::orpc::{OrpcThat, OrpcThis, StdObjRef};
/// `IRemUnknown` IID `00000131-0000-0000-C000-000000000046`
/// (`RemUnknownMessages.cs:7`).
pub const IREM_UNKNOWN_IID: Guid = Guid::new([
0x31, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46,
]);
/// `RemQueryInterface` opnum (`RemUnknownMessages.cs:8`).
pub const REM_QUERY_INTERFACE_OPNUM: u16 = 3;
/// `RemAddRef` opnum (`RemUnknownMessages.cs:9`).
pub const REM_ADD_REF_OPNUM: u16 = 4;
/// `RemRelease` opnum (`RemUnknownMessages.cs:10`).
pub const REM_RELEASE_OPNUM: u16 = 5;
/// Total length of an encoded `RemQueryInterface` request body for a single
/// requested IID. `OrpcThis(32) + ipid(16) + public_refs(4) + iid_count(2) +
/// align(2) + max_count(4) + iid(16) = 76`. Mirrors the byte-by-byte sum in
/// `RemUnknownMessages.cs:15-32`.
const REM_QUERY_INTERFACE_REQUEST_LEN: usize = OrpcThis::ENCODED_LEN + 16 + 4 + 2 + 2 + 4 + 16;
const _: () = assert!(REM_QUERY_INTERFACE_REQUEST_LEN == 76);
/// Encode a `RemQueryInterface` request body for a single requested IID.
///
/// Mirrors `EncodeRemQueryInterfaceRequest` (`RemUnknownMessages.cs:12-33`).
/// Layout:
///
/// ```text
/// offset size field
/// 0 32 OrpcThis (header)
/// 32 16 source IPID (GUID)
/// 48 4 public_refs u32 LE
/// 52 2 iid_count u16 LE = 1
/// 54 2 NDR alignment 0xCE 0xCE (RemUnknownMessages.cs:26-27)
/// 56 4 max_count u32 LE = 1 (conformant array max count)
/// 60 16 requested IID (GUID)
/// ```
///
/// Native passes `public_refs = 5` by default (`RemUnknownMessages.cs:12`);
/// the Rust signature requires the caller to pass it explicitly so the
/// default isn't accidentally hidden.
#[must_use]
pub fn encode_rem_query_interface_request(
source_ipid: Guid,
requested_iid: Guid,
causality_id: Guid,
public_refs: u32,
) -> Vec<u8> {
let orpc_this = OrpcThis::create(causality_id, None).encode();
let mut body = Vec::with_capacity(REM_QUERY_INTERFACE_REQUEST_LEN);
// 0..32 — OrpcThis header.
body.extend_from_slice(&orpc_this);
// 32..48 — source IPID.
body.extend_from_slice(source_ipid.as_bytes());
// 48..52 — public refs (default 5 in native).
body.extend_from_slice(&public_refs.to_le_bytes());
// 52..54 — iid count = 1.
body.extend_from_slice(&1u16.to_le_bytes());
// 54..56 — NDR alignment before the conformant array max count
// (`RemUnknownMessages.cs:26-27`).
body.push(0xCE);
body.push(0xCE);
// 56..60 — max count = 1.
body.extend_from_slice(&1u32.to_le_bytes());
// 60..76 — requested IID.
body.extend_from_slice(requested_iid.as_bytes());
debug_assert_eq!(body.len(), REM_QUERY_INTERFACE_REQUEST_LEN);
body
}
/// Decoded `RemQueryInterface` response body.
/// Mirrors `RemQueryInterfaceResponse` (`RemUnknownMessages.cs:62`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemQueryInterfaceResponse {
pub orpc_that: OrpcThat,
/// `Some` when the wire `referent_id` is non-zero
/// (`RemUnknownMessages.cs:46-50`); otherwise the server sent no
/// `REMQIRESULT` array.
pub result: Option<RemQiResult>,
/// Trailing status word at a position that depends on whether `result`
/// was parsed (`RemUnknownMessages.cs:52-58`).
pub error_code: u32,
}
/// `REMQIRESULT` body. Mirrors `RemQiResult` (`RemUnknownMessages.cs:64-79`).
///
/// ```text
/// offset size field
/// 0 4 hresult i32 LE
/// 4 4 pad_after_hresult [u8; 4] (NDR padding ahead of STDOBJREF;
/// `RemUnknownMessages.cs:75-77`
/// skips offsets 4..8)
/// 8 40 standard_object_reference (STDOBJREF)
/// ```
///
/// The 4 bytes between `hresult` and `standard_object_reference` are the
/// `IPID`-aligned NDR pad noted in `RemUnknownMessages.cs:77`. Native
/// reads-and-discards them; the Rust port preserves them as
/// `pad_after_hresult` per the CLAUDE.md "preserve unknown bytes" rule so
/// captures round-trip exactly.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RemQiResult {
pub hresult: i32,
pub pad_after_hresult: [u8; 4],
pub standard_object_reference: StdObjRef,
}
impl RemQiResult {
/// Encoded length — `4 + 4 + StdObjRef::ENCODED_LEN = 48`
/// (`RemUnknownMessages.cs:66`).
pub const ENCODED_LEN: usize = 4 + 4 + StdObjRef::ENCODED_LEN;
/// Decode 48 bytes. Mirrors `RemQiResult.Parse`
/// (`RemUnknownMessages.cs:68-78`). The 4 bytes at offsets 4..8 are
/// captured into `pad_after_hresult` rather than discarded
/// (CLAUDE.md "preserve unknown bytes").
///
/// # Errors
/// Returns [`RpcError::ShortRead`] if `buffer.len() < 48`.
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(),
});
}
let hresult = i32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
let mut pad_after_hresult = [0u8; 4];
pad_after_hresult.copy_from_slice(&buffer[4..8]);
let standard_object_reference = StdObjRef::parse(&buffer[8..Self::ENCODED_LEN])?;
Ok(Self {
hresult,
pad_after_hresult,
standard_object_reference,
})
}
/// Encode to 48 bytes. Native zeroes the 4-byte pad
/// (`RemUnknownMessages.cs` does not have a symmetric encoder, but the
/// pad slot is always 0 in captured server responses); the Rust port
/// writes whatever bytes the caller provided in `pad_after_hresult`.
#[must_use]
pub fn encode(&self) -> [u8; Self::ENCODED_LEN] {
let mut buf = [0u8; Self::ENCODED_LEN];
buf[0..4].copy_from_slice(&self.hresult.to_le_bytes());
buf[4..8].copy_from_slice(&self.pad_after_hresult);
buf[8..Self::ENCODED_LEN].copy_from_slice(&self.standard_object_reference.encode());
buf
}
}
/// Minimum length of a `RemQueryInterface` response: `OrpcThat(8) +
/// referent_id(4) + REMQIRESULT(48) + error_code(4) = 64`. Mirrors the
/// pre-check at `RemUnknownMessages.cs:37`.
const REM_QUERY_INTERFACE_RESPONSE_MIN_LEN: usize =
OrpcThat::ENCODED_LEN + 4 + RemQiResult::ENCODED_LEN + 4;
const _: () = assert!(REM_QUERY_INTERFACE_RESPONSE_MIN_LEN == 64);
/// Decode a `RemQueryInterface` response body.
///
/// Mirrors `ParseRemQueryInterfaceResponse` (`RemUnknownMessages.cs:35-59`).
/// The `referent_id != 0` branch (`RemUnknownMessages.cs:46-50`) is the Q7
/// conditional read called out in `design/70-risks-and-open-questions.md:283-289`:
/// the `REMQIRESULT` array is parsed only when `referent_id != 0`, and the
/// trailing `error_code` lives at a different offset depending on whether
/// it was parsed (`RemUnknownMessages.cs:52-58`).
///
/// # Errors
/// Returns [`RpcError::ShortRead`] if the buffer is shorter than the
/// 64-byte minimum, or [`RpcError::Decode`] if the trailing `error_code`
/// runs past the buffer (the conditional path makes this possible even
/// when the minimum length is met).
pub fn parse_rem_query_interface_response(
buffer: &[u8],
) -> Result<RemQueryInterfaceResponse, RpcError> {
if buffer.len() < REM_QUERY_INTERFACE_RESPONSE_MIN_LEN {
return Err(RpcError::ShortRead {
expected: REM_QUERY_INTERFACE_RESPONSE_MIN_LEN,
actual: buffer.len(),
});
}
let orpc_that = OrpcThat::parse(&buffer[..OrpcThat::ENCODED_LEN])?;
let referent_id_offset = OrpcThat::ENCODED_LEN;
let referent_id = u32::from_le_bytes([
buffer[referent_id_offset],
buffer[referent_id_offset + 1],
buffer[referent_id_offset + 2],
buffer[referent_id_offset + 3],
]);
let mut offset = referent_id_offset + 4;
let result = if referent_id != 0 {
// Conformant array max count for the REMQIRESULT result array
// (`RemUnknownMessages.cs:48`).
offset += 4;
if buffer.len() < offset + RemQiResult::ENCODED_LEN {
return Err(RpcError::Decode {
offset,
reason: "RemQueryInterface response truncated before REMQIRESULT",
buffer_len: buffer.len(),
});
}
let parsed = RemQiResult::parse(&buffer[offset..offset + RemQiResult::ENCODED_LEN])?;
offset += RemQiResult::ENCODED_LEN;
Some(parsed)
} else {
None
};
if buffer.len() < offset + 4 {
return Err(RpcError::Decode {
offset,
reason: "RemQueryInterface response truncated before error_code",
buffer_len: buffer.len(),
});
}
let error_code = u32::from_le_bytes([
buffer[offset],
buffer[offset + 1],
buffer[offset + 2],
buffer[offset + 3],
]);
Ok(RemQueryInterfaceResponse {
orpc_that,
result,
error_code,
})
}
// ---- F11 — RemAddRef / RemRelease (opnums 4 and 5) -----------------------
//
// `[MS-DCOM]` §3.1.1.5.6 specifies both as taking a `REMINTERFACEREF[]` array
// and returning per-element `HRESULT`s. The .NET reference declares the
// opnums (`RemUnknownMessages.cs:9-10`) but ships no encoders/decoders; this
// is a structural port from the spec. Validate against a captured frame
// when one becomes available — until then these are ship-by-design.
//
// `[MS-DCOM]` §2.2.19 `REMINTERFACEREF`:
// - ipid: IPID (GUID, 16 bytes)
// - cPublicRefs: u32 LE
// - cPrivateRefs: u32 LE
//
// Total: 24 bytes per element. The array is wire-encoded as a 4-byte LE
// `count` followed by `count × 24` bytes of `REMINTERFACEREF` records.
/// One `REMINTERFACEREF` entry per `[MS-DCOM]` §2.2.19. Used as the
/// element type for both `RemAddRef` and `RemRelease` request bodies.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RemInterfaceRef {
pub ipid: Guid,
pub public_refs: u32,
pub private_refs: u32,
}
impl RemInterfaceRef {
/// Encoded length on the wire — `[MS-DCOM]` §2.2.19.
pub const ENCODED_LEN: usize = 16 + 4 + 4;
}
/// Encode a `RemAddRef` request body. Layout mirrors `[MS-DCOM]`
/// §3.1.1.5.6.1:
///
/// ```text
/// offset size field
/// 0 32 OrpcThis
/// 32 2 refs_count u16 LE = N
/// 34 2 NDR alignment 0xCE 0xCE
/// 36 4 max_count u32 LE = N (conformant array max)
/// 40 .. N × 24-byte REMINTERFACEREF
/// ```
///
/// Wire shape mirrors the existing `RemQueryInterface` request (which
/// is opnum 3 on the same interface) — same alignment + conformant-
/// array convention.
#[must_use]
pub fn encode_rem_add_ref_request(refs: &[RemInterfaceRef], causality_id: Guid) -> Vec<u8> {
encode_remref_array_request(refs, causality_id)
}
/// Encode a `RemRelease` request body. Same wire shape as
/// `RemAddRef` per `[MS-DCOM]` §3.1.1.5.6.2.
#[must_use]
pub fn encode_rem_release_request(refs: &[RemInterfaceRef], causality_id: Guid) -> Vec<u8> {
encode_remref_array_request(refs, causality_id)
}
fn encode_remref_array_request(refs: &[RemInterfaceRef], causality_id: Guid) -> Vec<u8> {
let count = refs.len();
let body_len = OrpcThis::ENCODED_LEN
+ 2 // refs_count u16
+ 2 // NDR padding 0xCE 0xCE
+ 4 // max_count u32
+ count * RemInterfaceRef::ENCODED_LEN;
let mut body = Vec::with_capacity(body_len);
body.extend_from_slice(&OrpcThis::create(causality_id, None).encode());
let count_u16 = u16::try_from(count).unwrap_or(u16::MAX);
let count_u32 = u32::try_from(count).unwrap_or(u32::MAX);
body.extend_from_slice(&count_u16.to_le_bytes());
body.push(0xCE);
body.push(0xCE);
body.extend_from_slice(&count_u32.to_le_bytes());
for r in refs {
body.extend_from_slice(r.ipid.as_bytes());
body.extend_from_slice(&r.public_refs.to_le_bytes());
body.extend_from_slice(&r.private_refs.to_le_bytes());
}
body
}
/// Decoded `RemAddRef` / `RemRelease` response body. Both ops share
/// the same `OrpcThat + HRESULT[] + tail-status` shape per
/// `[MS-DCOM]` §3.1.1.5.6.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemRefResponse {
pub orpc_that: OrpcThat,
/// Per-element HRESULTs, one per input `REMINTERFACEREF`. Length
/// matches the request's `refs.len()` — when 0 the array is empty
/// and the response just carries the trailing error code.
pub per_ref_hresults: Vec<i32>,
/// Trailing 4-byte error code (DCE/RPC-level status).
pub error_code: u32,
}
/// Decode a `RemAddRef` / `RemRelease` response body. Wire layout:
///
/// ```text
/// offset size field
/// 0 8 OrpcThat
/// 8 4 referent_id u32 LE (0 ⇒ no array)
/// 12 4 max_count u32 LE (when referent_id != 0)
/// 16 .. N × 4-byte HRESULT
/// ... 4 error_code u32 LE
/// ```
///
/// Mirrors the conformant-array conventions established by the
/// existing `RemQueryInterface` response decoder (`referent_id == 0`
/// → no array; `error_code` lives at a different offset depending
/// on whether the array was parsed).
///
/// # Errors
///
/// - [`RpcError::ShortRead`] when the buffer is shorter than the
/// minimum (12 bytes — OrpcThat + referent_id + 4-byte status).
/// - [`RpcError::Decode`] when `max_count × 4` would run past the
/// buffer, i.e. the conformant array is truncated.
pub fn parse_remref_response(buffer: &[u8]) -> Result<RemRefResponse, RpcError> {
const MIN_LEN: usize = OrpcThat::ENCODED_LEN + 4 + 4;
if buffer.len() < MIN_LEN {
return Err(RpcError::ShortRead {
expected: MIN_LEN,
actual: buffer.len(),
});
}
let orpc_that = OrpcThat::parse(&buffer[..OrpcThat::ENCODED_LEN])?;
let mut offset = OrpcThat::ENCODED_LEN;
let referent_id = u32::from_le_bytes([
buffer[offset],
buffer[offset + 1],
buffer[offset + 2],
buffer[offset + 3],
]);
offset += 4;
let per_ref_hresults = if referent_id == 0 {
Vec::new()
} else {
if offset + 4 > buffer.len() {
return Err(RpcError::ShortRead {
expected: offset + 4,
actual: buffer.len(),
});
}
let max_count = u32::from_le_bytes([
buffer[offset],
buffer[offset + 1],
buffer[offset + 2],
buffer[offset + 3],
]) as usize;
offset += 4;
let array_bytes = max_count.checked_mul(4).ok_or(RpcError::Decode {
offset,
reason: "RemRef HRESULT[] count overflows usize",
buffer_len: buffer.len(),
})?;
if offset + array_bytes + 4 > buffer.len() {
return Err(RpcError::Decode {
offset,
reason: "RemRef HRESULT[] is truncated",
buffer_len: buffer.len(),
});
}
let mut out = Vec::with_capacity(max_count);
for i in 0..max_count {
let p = offset + i * 4;
out.push(i32::from_le_bytes([
buffer[p],
buffer[p + 1],
buffer[p + 2],
buffer[p + 3],
]));
}
offset += array_bytes;
out
};
if offset + 4 > buffer.len() {
return Err(RpcError::ShortRead {
expected: offset + 4,
actual: buffer.len(),
});
}
let error_code = u32::from_le_bytes([
buffer[offset],
buffer[offset + 1],
buffer[offset + 2],
buffer[offset + 3],
]);
Ok(RemRefResponse {
orpc_that,
per_ref_hresults,
error_code,
})
}
#[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)
}
fn sample_std_objref() -> StdObjRef {
StdObjRef {
flags: 0,
public_refs: 5,
oxid: 0x1122_3344_5566_7788,
oid: 0x99AA_BBCC_DDEE_FF00,
ipid: sample_guid(0x55),
}
}
#[test]
fn irem_unknown_iid_matches_dotnet() {
// RemUnknownMessages.cs:7 — 00000131-0000-0000-C000-000000000046.
assert_eq!(
IREM_UNKNOWN_IID.as_bytes(),
&[
0x31, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x46,
]
);
// Display order also matches Guid.ToString("D").
assert_eq!(
IREM_UNKNOWN_IID.to_string(),
"00000131-0000-0000-c000-000000000046"
);
}
#[test]
fn opnums_match_dotnet() {
assert_eq!(REM_QUERY_INTERFACE_OPNUM, 3);
assert_eq!(REM_ADD_REF_OPNUM, 4);
assert_eq!(REM_RELEASE_OPNUM, 5);
}
#[test]
fn encode_rem_query_interface_request_layout() {
let source_ipid = sample_guid(0x10);
let requested_iid = sample_guid(0x20);
let causality_id = sample_guid(0x30);
let body = encode_rem_query_interface_request(source_ipid, requested_iid, causality_id, 5);
// 32 (OrpcThis) + 16 (ipid) + 4 (refs) + 2 (count) + 2 (align) + 4 (max) + 16 (iid).
assert_eq!(body.len(), 76);
// OrpcThis header round-trip (validates the first 32 bytes).
let parsed_this = OrpcThis::parse(&body[..OrpcThis::ENCODED_LEN]).unwrap();
assert_eq!(parsed_this.cid, causality_id);
assert_eq!(parsed_this.flags, 0);
assert_eq!(parsed_this.extensions_referent_id, 0);
// Source IPID at offset 32.
assert_eq!(&body[32..48], source_ipid.as_bytes());
// public_refs at offset 48.
assert_eq!(&body[48..52], &5u32.to_le_bytes());
// iid_count at offset 52.
assert_eq!(&body[52..54], &1u16.to_le_bytes());
// NDR alignment 0xCE 0xCE at offset 54 (RemUnknownMessages.cs:26-27).
assert_eq!(body[54], 0xCE);
assert_eq!(body[55], 0xCE);
// max_count at offset 56.
assert_eq!(&body[56..60], &1u32.to_le_bytes());
// requested IID at offset 60.
assert_eq!(&body[60..76], requested_iid.as_bytes());
}
#[test]
fn encode_rem_query_interface_request_respects_public_refs() {
let body =
encode_rem_query_interface_request(Guid::ZERO, Guid::ZERO, Guid::ZERO, 0xDEAD_BEEF);
assert_eq!(&body[48..52], &0xDEAD_BEEFu32.to_le_bytes());
}
#[test]
fn rem_qi_result_round_trip() {
let original = RemQiResult {
hresult: 0,
pad_after_hresult: [0xAA, 0xBB, 0xCC, 0xDD],
standard_object_reference: sample_std_objref(),
};
let encoded = original.encode();
assert_eq!(encoded.len(), RemQiResult::ENCODED_LEN);
assert_eq!(encoded.len(), 48);
// Pad bytes preserved exactly (CLAUDE.md "preserve unknown bytes").
assert_eq!(&encoded[4..8], &[0xAA, 0xBB, 0xCC, 0xDD]);
let decoded = RemQiResult::parse(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn rem_qi_result_short_buffer_errors() {
assert!(matches!(
RemQiResult::parse(&[0u8; 47]),
Err(RpcError::ShortRead {
expected: 48,
actual: 47
})
));
}
#[test]
fn parse_response_referent_id_zero_skips_result() {
// Layout when referent_id == 0:
// 0..8 OrpcThat
// 8..12 referent_id = 0
// 12..16 error_code
// Native (`RemUnknownMessages.cs:46-58`): when referent_id == 0,
// result is None and error_code is read from offset 12 directly.
// The pre-check at :37 still requires a 64-byte buffer, so we pad
// the trailing portion with junk that the parser must ignore once
// it has the error_code at offset 12.
let mut buf = vec![0u8; REM_QUERY_INTERFACE_RESPONSE_MIN_LEN];
// OrpcThat
buf[0..4].copy_from_slice(&0u32.to_le_bytes());
buf[4..8].copy_from_slice(&0u32.to_le_bytes());
// referent_id = 0
buf[8..12].copy_from_slice(&0u32.to_le_bytes());
// error_code at offset 12 in this branch.
buf[12..16].copy_from_slice(&0x8000_4005u32.to_le_bytes());
let resp = parse_rem_query_interface_response(&buf).unwrap();
assert!(resp.result.is_none());
assert_eq!(resp.error_code, 0x8000_4005);
}
#[test]
fn parse_response_referent_id_nonzero_parses_result() {
// Layout when referent_id != 0:
// 0..8 OrpcThat
// 8..12 referent_id != 0
// 12..16 conformant-array max_count (skipped per :48)
// 16..64 REMQIRESULT
// 64..68 error_code
let std_ref = sample_std_objref();
let inner = RemQiResult {
hresult: 0,
pad_after_hresult: [0u8; 4],
standard_object_reference: std_ref,
};
let mut buf = vec![0u8; OrpcThat::ENCODED_LEN + 4 + 4 + RemQiResult::ENCODED_LEN + 4];
// OrpcThat
buf[0..4].copy_from_slice(&0u32.to_le_bytes());
buf[4..8].copy_from_slice(&0u32.to_le_bytes());
// referent_id != 0
buf[8..12].copy_from_slice(&0x0002_0000u32.to_le_bytes());
// max_count = 1 (skipped after read).
buf[12..16].copy_from_slice(&1u32.to_le_bytes());
// REMQIRESULT body at 16..64.
buf[16..16 + RemQiResult::ENCODED_LEN].copy_from_slice(&inner.encode());
// error_code at offset 64.
let err_off = 16 + RemQiResult::ENCODED_LEN;
buf[err_off..err_off + 4].copy_from_slice(&0u32.to_le_bytes());
let resp = parse_rem_query_interface_response(&buf).unwrap();
assert_eq!(resp.error_code, 0);
let parsed = resp.result.expect("result present when referent_id != 0");
assert_eq!(parsed.hresult, 0);
assert_eq!(parsed.standard_object_reference, std_ref);
// The error_code lives at offset 64 in this branch:
// OrpcThat(8) + referent_id(4) + max_count(4) + REMQIRESULT(48) = 64.
assert_eq!(err_off, 64);
}
#[test]
fn parse_response_short_buffer_errors() {
// 63 bytes — one short of the 64-byte minimum (`:37`).
let buf = vec![0u8; REM_QUERY_INTERFACE_RESPONSE_MIN_LEN - 1];
let err = parse_rem_query_interface_response(&buf).unwrap_err();
assert!(matches!(
err,
RpcError::ShortRead {
expected: 64,
actual: 63
}
));
}
#[test]
fn parse_response_preserves_orpc_that() {
let mut buf = vec![0u8; REM_QUERY_INTERFACE_RESPONSE_MIN_LEN];
buf[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
buf[4..8].copy_from_slice(&0x1234_5678u32.to_le_bytes());
// referent_id = 0 so we don't need to populate the rest.
let resp = parse_rem_query_interface_response(&buf).unwrap();
assert_eq!(resp.orpc_that.flags, 0xDEAD_BEEF);
assert_eq!(resp.orpc_that.extensions_referent_id, 0x1234_5678);
}
// ---- F11 — RemAddRef / RemRelease structural tests --------------
#[test]
fn encode_rem_add_ref_request_layout_matches_spec() {
let refs = vec![
RemInterfaceRef {
ipid: sample_guid(0x10),
public_refs: 2,
private_refs: 0,
},
RemInterfaceRef {
ipid: sample_guid(0x20),
public_refs: 5,
private_refs: 1,
},
];
let body = encode_rem_add_ref_request(&refs, sample_guid(0xCC));
// OrpcThis(32) + 2 (count u16) + 2 (NDR pad) + 4 (max_count) + 2*24 = 88
assert_eq!(body.len(), 32 + 2 + 2 + 4 + 2 * RemInterfaceRef::ENCODED_LEN);
// refs_count u16 LE = 2
assert_eq!(&body[32..34], &[0x02, 0x00]);
// NDR padding 0xCE 0xCE
assert_eq!(&body[34..36], &[0xCE, 0xCE]);
// max_count u32 LE = 2
assert_eq!(&body[36..40], &[0x02, 0x00, 0x00, 0x00]);
// First REMINTERFACEREF: ipid(16) + public_refs(4) + private_refs(4)
assert_eq!(&body[40..56], sample_guid(0x10).as_bytes());
assert_eq!(&body[56..60], &2u32.to_le_bytes());
assert_eq!(&body[60..64], &0u32.to_le_bytes());
}
#[test]
fn encode_rem_release_request_uses_same_shape() {
// RemRelease and RemAddRef share the wire layout per spec.
let refs = vec![RemInterfaceRef {
ipid: sample_guid(0x40),
public_refs: 5,
private_refs: 0,
}];
let cid = sample_guid(0xAA);
let add = encode_rem_add_ref_request(&refs, cid);
let release = encode_rem_release_request(&refs, cid);
assert_eq!(add, release);
}
#[test]
fn parse_remref_response_two_hresults_round_trip() {
// Build a valid response: OrpcThat(8) + referent_id(non-zero, 4) +
// max_count(2, 4) + 2 × HRESULT(4) + error_code(4) = 28
let mut buf = vec![0u8; 28];
// OrpcThat: zero-fill works fine (flags=0, ext_referent_id=0).
// referent_id != 0 so the array gets parsed.
buf[8..12].copy_from_slice(&1u32.to_le_bytes());
// max_count = 2
buf[12..16].copy_from_slice(&2u32.to_le_bytes());
// HRESULT[0] = 0
buf[16..20].copy_from_slice(&0i32.to_le_bytes());
// HRESULT[1] = 0x80004005 (E_FAIL)
buf[20..24].copy_from_slice(&(0x8000_4005u32 as i32).to_le_bytes());
// error_code = 0
buf[24..28].copy_from_slice(&0u32.to_le_bytes());
let resp = parse_remref_response(&buf).unwrap();
assert_eq!(resp.per_ref_hresults.len(), 2);
assert_eq!(resp.per_ref_hresults[0], 0);
assert_eq!(resp.per_ref_hresults[1] as u32, 0x8000_4005);
assert_eq!(resp.error_code, 0);
}
#[test]
fn parse_remref_response_referent_zero_skips_array() {
// Minimal: OrpcThat(8) + referent_id(0) + error_code(4) = 16
let mut buf = vec![0u8; 16];
buf[8..12].copy_from_slice(&0u32.to_le_bytes()); // referent_id = 0
buf[12..16].copy_from_slice(&0xCAFE_BABEu32.to_le_bytes());
let resp = parse_remref_response(&buf).unwrap();
assert!(resp.per_ref_hresults.is_empty());
assert_eq!(resp.error_code, 0xCAFE_BABE);
}
#[test]
fn parse_remref_response_short_buffer_errors() {
// Below the 16-byte minimum.
let buf = vec![0u8; 10];
assert!(matches!(
parse_remref_response(&buf),
Err(RpcError::ShortRead { .. })
));
}
}