[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:
@@ -0,0 +1,469 @@
|
||||
//! `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,
|
||||
})
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user