//! `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 { 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, /// 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 { 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 { 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 { 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 { encode_remref_array_request(refs, causality_id) } fn encode_remref_array_request(refs: &[RemInterfaceRef], causality_id: Guid) -> Vec { 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, /// 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 { 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 { .. }) )); } }