//! `IObjectExporter` body codec — `ResolveOxid` request/response. //! //! Direct port of the codec-only members of //! `src/MxNativeClient/ObjectExporterMessages.cs`. This module covers: //! //! - The `IObjectExporter` interface IID and opnum constants //! (`ObjectExporterMessages.cs:7-13`). //! - DCE/RPC protocol-sequence ids used by `ResolveOxid` //! (`ObjectExporterMessages.cs:15-16`, `[MS-DCOM]` §2.2.10). //! - [`encode_resolve_oxid_request`] — produces the marshalled request stub //! for `IObjectExporter::ResolveOxid` (opnum 0). Mirrors //! `EncodeResolveOxidRequest` (`ObjectExporterMessages.cs:18-37`). //! - [`parse_resolve_oxid_failure`] — extracts the trailing 4-byte error //! status from a failure response stub. Mirrors //! `ParseResolveOxidFailure` (`ObjectExporterMessages.cs:39-47`). //! - [`parse_resolve_oxid_result`] — decodes the success-shape response //! stub (DUALSTRINGARRAY of bindings + IPID + authn-hint + status). //! Mirrors `ParseResolveOxidResult` (`ObjectExporterMessages.cs:49-90`). //! //! **Not ported here:** `src/MxNativeClient/ObjectExporterClient.cs`. Those //! four methods (`ResolveOxidUnauthenticated`, //! `ResolveOxidWithNtlmConnect`, `ResolveOxidWithNtlmPacketIntegrity`, //! `ResolveOxidWithManagedNtlmPacketIntegrity`) are transport-layer code //! that depend on a `DceRpcTcpClient` we have not yet ported. They will //! follow once the transport crate exists. //! //! The dual-string-array decode in this module is intentionally **not** //! consolidated with [`crate::objref::ComObjRef`]'s decoder. The two //! shapes differ in three documented ways //! (`ObjectExporterMessages.cs:92-126`): //! //! 1. The loop iterates `entries` u16 code units exactly — **not** //! `min(entries, data.len()/2)` like the OBJREF parser //! (`ComObjRef.cs:59`). The caller is responsible for slicing the input //! to the expected byte length up front. //! 2. Non-printable code units are escaped as a single `'?'` character — //! **not** the `` lowercase-hex form used by `ComObjRef`. //! 3. The protocol label is either `"ncacn_ip_tcp"` (for `0x0007`) or a //! decimal-formatted `"protseq_0x{:04x}"` fallback — there is no other //! tower-id table. // Direct byte indexing — every access is guarded by an explicit length check // and the result reads as a 1:1 mirror of the .NET `BinaryPrimitives` calls. // `.get(n)?` would obscure the byte map. Mirrors the rationale documented in // `crates/mxaccess-codec/src/reference_handle.rs:7-11` and `objref.rs:25`. #![allow(clippy::indexing_slicing)] use crate::error::RpcError; use crate::guid::Guid; use crate::objref::ComDualStringEntry; /// `IObjectExporter` IID `99FCFEC4-5260-101B-BBCB-00AA0021347A` /// (`ObjectExporterMessages.cs:7`, `[MS-DCOM]` §1.9). The wire bytes are /// .NET `Guid.TryWriteBytes(span)` order: first three groups /// little-endian (`Data1` u32 LE, `Data2` u16 LE, `Data3` u16 LE) followed /// by 8 big-endian `Data4` bytes. pub const IOBJECT_EXPORTER_IID: Guid = Guid::new([ 0xC4, 0xFE, 0xFC, 0x99, 0x60, 0x52, 0x1B, 0x10, 0xBB, 0xCB, 0x00, 0xAA, 0x00, 0x21, 0x34, 0x7A, ]); /// Opnum 0 — `ResolveOxid` (`ObjectExporterMessages.cs:8`, /// `[MS-DCOM]` §3.1.2.5.1.1). pub const RESOLVE_OXID_OPNUM: u16 = 0; /// Opnum 1 — `SimplePing` (`ObjectExporterMessages.cs:9`). pub const SIMPLE_PING_OPNUM: u16 = 1; /// Opnum 2 — `ComplexPing` (`ObjectExporterMessages.cs:10`). pub const COMPLEX_PING_OPNUM: u16 = 2; /// Opnum 3 — `ServerAlive` (`ObjectExporterMessages.cs:11`). pub const SERVER_ALIVE_OPNUM: u16 = 3; /// Opnum 4 — `ResolveOxid2` (`ObjectExporterMessages.cs:12`). pub const RESOLVE_OXID2_OPNUM: u16 = 4; /// Opnum 5 — `ServerAlive2` (`ObjectExporterMessages.cs:13`). pub const SERVER_ALIVE2_OPNUM: u16 = 5; /// Protocol sequence `ncacn_ip_tcp` (`ObjectExporterMessages.cs:15`, /// `[MS-DCOM]` §2.2.10). pub const PROTSEQ_NCACN_IP_TCP: u16 = 0x0007; /// Protocol sequence `ncalrpc` (`ObjectExporterMessages.cs:16`). pub const PROTSEQ_NCALRPC: u16 = 0x001f; /// 4-byte alignment helper. Mirrors `Align` /// (`ObjectExporterMessages.cs:128-132`). const fn align(value: usize, alignment: usize) -> usize { let remainder = value % alignment; if remainder == 0 { value } else { value + alignment - remainder } } /// Encode the `IObjectExporter::ResolveOxid` request stub. /// /// Wire layout (`ObjectExporterMessages.cs:18-37`): /// /// ```text /// offset size field /// 0 8 oxid u64 LE /// 8 2 count (short) u16 LE (= requested_protseqs.len()) /// 10 2 u16 (zero — implicit from buffer init) /// 12 4 count (max) u32 LE (= requested_protseqs.len()) /// 16 N*2 protseqs[] u16 LE each /// ``` /// /// The buffer length is then 4-byte aligned per `Align(length, 4)` /// (`:26`); for an odd-length protseq array this adds 2 trailing zero /// bytes. /// /// # Errors /// /// Returns [`RpcError::Decode`] if `requested_protseqs` is empty — /// mirrors the .NET `ArgumentException` at /// `ObjectExporterMessages.cs:21-23`. pub fn encode_resolve_oxid_request( oxid: u64, requested_protseqs: &[u16], ) -> Result, RpcError> { if requested_protseqs.is_empty() { return Err(RpcError::Decode { offset: 0, reason: "ResolveOxid request requires at least one protseq", buffer_len: 0, }); } // u16 protseq array — `len * 2` is identical to .NET's // `requestedProtseqs.Count * sizeof(ushort)` (cs:25). let mut length = 8 + 2 + 2 + 4 + std::mem::size_of_val(requested_protseqs); length = align(length, 4); let mut buffer = vec![0u8; length]; buffer[0..8].copy_from_slice(&oxid.to_le_bytes()); // Truncating cast mirrors the .NET `(ushort)requestedProtseqs.Count`. let count_u16: u16 = (requested_protseqs.len() as u32) as u16; buffer[8..10].copy_from_slice(&count_u16.to_le_bytes()); let count_u32: u32 = requested_protseqs.len() as u32; buffer[12..16].copy_from_slice(&count_u32.to_le_bytes()); for (i, ps) in requested_protseqs.iter().enumerate() { let off = 16 + i * size_of::(); buffer[off..off + 2].copy_from_slice(&ps.to_le_bytes()); } Ok(buffer) } /// Failure-shape response of `IObjectExporter::ResolveOxid` — only the /// trailing 4-byte HRESULT/`error_status` is meaningful. /// /// Mirrors `ResolveOxidFailure` (`ObjectExporterMessages.cs:135`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ResolveOxidFailure { pub error_status: u32, } /// Success-shape response of `IObjectExporter::ResolveOxid` — the /// DUALSTRINGARRAY of server bindings + IPID for `IRemUnknown` + /// authn-svc hint + final status. /// /// Mirrors `ResolveOxidResult` (`ObjectExporterMessages.cs:137-141`). #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ResolveOxidResult { pub bindings: Vec, pub rem_unknown_ipid: Guid, pub authn_hint: u32, pub error_status: u32, } /// Parse a failure-shape `ResolveOxid` response stub. The 4-byte status /// sits at the **end** of the stub (`stub[^4..]`, /// `ObjectExporterMessages.cs:46`). /// /// # Errors /// /// Returns [`RpcError::ShortRead`] if the stub is shorter than 4 bytes — /// mirrors the .NET `ArgumentException` at /// `ObjectExporterMessages.cs:41-44`. pub fn parse_resolve_oxid_failure(stub: &[u8]) -> Result { if stub.len() < 4 { return Err(RpcError::ShortRead { expected: 4, actual: stub.len(), }); } let tail = &stub[stub.len() - 4..]; Ok(ResolveOxidFailure { error_status: u32::from_le_bytes([tail[0], tail[1], tail[2], tail[3]]), }) } /// Parse a success-shape `ResolveOxid` response stub. /// /// Wire layout (`ObjectExporterMessages.cs:49-90`): /// /// ```text /// offset size field /// 0 4 referent_id u32 LE /// 4 4 max_count u32 LE (NDR conformant array max) /// 8 2 entries u16 LE (DUALSTRINGARRAY wNumEntries) /// 10 2 security_offset u16 LE (DUALSTRINGARRAY wSecurityOffset) /// 12 .. dual-string array u16 LE each, length = entries * 2 bytes /// ... .. padding to next 4-byte boundary /// ... 16 rem_unknown_ipid GUID /// ... 4 authn_hint u32 LE /// ... 4 error_status u32 LE /// ``` /// /// Notable behaviors mirrored from the .NET source: /// /// - If `referent_id == 0` the bindings are empty, IPID is zero, authn /// hint is zero, and the error status is read from the **trailing** 4 /// bytes (`ObjectExporterMessages.cs:57-61`). /// - If `max_count < entries` the input is rejected /// (`ObjectExporterMessages.cs:66-69`). /// - `arrayBytes = max_count * sizeof(u16)` is the conformant-array byte /// length; the dual-string decode is sliced to `entries * 2` bytes /// (`:78`). The trailing fields read offset is then 4-byte aligned /// (`:79`). /// /// # Errors /// /// - [`RpcError::ShortRead`] when `stub.len() < 32` /// (`ObjectExporterMessages.cs:51-54`). /// - [`RpcError::Decode`] when `max_count < entries` /// (`:66-69`), the conformant array runs past the buffer (`:73-76`), /// or the trailing 24 bytes are truncated (`:80-83`). pub fn parse_resolve_oxid_result(stub: &[u8]) -> Result { if stub.len() < 32 { return Err(RpcError::ShortRead { expected: 32, actual: stub.len(), }); } let referent_id = u32::from_le_bytes([stub[0], stub[1], stub[2], stub[3]]); if referent_id == 0 { let tail = &stub[stub.len() - 4..]; let null_status = u32::from_le_bytes([tail[0], tail[1], tail[2], tail[3]]); return Ok(ResolveOxidResult { bindings: Vec::new(), rem_unknown_ipid: Guid::ZERO, authn_hint: 0, error_status: null_status, }); } let max_count = u32::from_le_bytes([stub[4], stub[5], stub[6], stub[7]]); let entries = u16::from_le_bytes([stub[8], stub[9]]); let security_offset = u16::from_le_bytes([stub[10], stub[11]]); if (max_count as u64) < (entries as u64) { return Err(RpcError::Decode { offset: 4, reason: "ResolveOxid DUALSTRINGARRAY max count is smaller than entry count", buffer_len: stub.len(), }); } let array_offset: usize = 12; // `checked((int)maxCount * sizeof(ushort))` (`:72`). max_count fits in // u32; multiplying by 2 fits in u64 with no overflow on any platform. let array_bytes: usize = match (max_count as usize).checked_mul(2) { Some(n) => n, None => { return Err(RpcError::Decode { offset: 4, reason: "ResolveOxid DUALSTRINGARRAY max count overflows usize", buffer_len: stub.len(), }); } }; if array_offset .checked_add(array_bytes) .is_none_or(|end| end > stub.len()) { return Err(RpcError::Decode { offset: array_offset, reason: "ResolveOxid DUALSTRINGARRAY is truncated", buffer_len: stub.len(), }); } let entries_bytes: usize = (entries as usize) * 2; let array_slice = &stub[array_offset..array_offset + entries_bytes]; let decoded = decode_dual_string_array(array_slice, entries, security_offset); let offset = align(array_offset + array_bytes, 4); if offset.checked_add(24).is_none_or(|end| end > stub.len()) { return Err(RpcError::Decode { offset, reason: "ResolveOxid trailing fields are truncated", buffer_len: stub.len(), }); } let ipid = Guid::parse(&stub[offset..offset + 16])?; let authn_hint = u32::from_le_bytes([ stub[offset + 16], stub[offset + 17], stub[offset + 18], stub[offset + 19], ]); let error_status = u32::from_le_bytes([ stub[offset + 20], stub[offset + 21], stub[offset + 22], stub[offset + 23], ]); Ok(ResolveOxidResult { bindings: decoded, rem_unknown_ipid: ipid, authn_hint, error_status, }) } /// Decode the dual-string-array slice produced by /// `IObjectExporter::ResolveOxid`. /// /// Mirrors `DecodeDualStringArray` (`ObjectExporterMessages.cs:92-126`). /// /// **This is intentionally a different shape than /// [`crate::objref::ComObjRef`]'s dual-string decoder.** Three differences /// vs. `ComObjRef.cs:57-102`: /// /// 1. The loop iterates `entries` u16 code units exactly. The caller is /// expected to have sliced `data` to `entries * 2` bytes already /// (`ObjectExporterMessages.cs:78`). /// 2. Non-printable code units are emitted as **`'?'`** rather than /// `` (`:115`). /// 3. The protocol label is either `"ncacn_ip_tcp"` (for tower id /// `0x0007`) or `format!("protseq_0x{:04x}", tower_id)` — no other /// tower table is consulted (`:120`). /// /// `is_security_binding` is set when the entry's start index (in u16 /// code units) is at or past `security_offset` (`:122`). pub fn decode_dual_string_array( data: &[u8], entries: u16, security_offset: u16, ) -> Vec { let entries = entries as usize; let mut strings = Vec::new(); let mut i: usize = 0; while i < entries { let entry_start = i; // Bound u16 reads to the supplied slice; the .NET source assumes // the caller pre-sliced to `entries * 2` and would otherwise throw // an `ArgumentOutOfRangeException`. Mirror that contract by // stopping early if the data was over-trimmed. if i * 2 + 2 > data.len() { break; } let tower_id = u16::from_le_bytes([data[i * 2], data[i * 2 + 1]]); i += 1; if tower_id == 0 { continue; } let mut text = String::new(); while i < entries { if i * 2 + 2 > data.len() { break; } let value = u16::from_le_bytes([data[i * 2], data[i * 2 + 1]]); i += 1; if value == 0 { break; } // `value >= 0x20 && value <= 0x7e ? (char)value : '?'` (:115). if (0x20..=0x7e).contains(&value) { text.push(value as u8 as char); } else { text.push('?'); } } // The canonical `"ncacn_ip_tcp"` label (tower 0x0007) is borrowed // from a `&'static str`; everything else is owned. `ComDualStringEntry::protocol` // is `Cow<'static, str>` — see the type-doc on that struct for why // the OBJREF and OXID parsers emit different protocol labels for // the same tower id. let protocol: std::borrow::Cow<'static, str> = if tower_id == PROTSEQ_NCACN_IP_TCP { std::borrow::Cow::Borrowed("ncacn_ip_tcp") } else { std::borrow::Cow::Owned(format!("protseq_0x{:04x}", tower_id)) }; strings.push(ComDualStringEntry { tower_id, protocol, value: text, is_security_binding: entry_start >= security_offset as usize, }); } strings } // Compile-time invariants: opnums and protseq constants match // `ObjectExporterMessages.cs:8-16`. const _: () = assert!(RESOLVE_OXID_OPNUM == 0); const _: () = assert!(SIMPLE_PING_OPNUM == 1); const _: () = assert!(COMPLEX_PING_OPNUM == 2); const _: () = assert!(SERVER_ALIVE_OPNUM == 3); const _: () = assert!(RESOLVE_OXID2_OPNUM == 4); const _: () = assert!(SERVER_ALIVE2_OPNUM == 5); const _: () = assert!(PROTSEQ_NCACN_IP_TCP == 0x0007); const _: () = assert!(PROTSEQ_NCALRPC == 0x001f); // Spot-check the IID wire layout: first byte is `Data1` LSB (0xC4) and // the trailing big-endian half of `Data4` ends in 0x7A. const _: () = assert!(IOBJECT_EXPORTER_IID.0[0] == 0xC4); const _: () = assert!(IOBJECT_EXPORTER_IID.0[15] == 0x7A); #[cfg(test)] #[allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] mod tests { use super::*; /// Wire bytes of `99FCFEC4-5260-101B-BBCB-00AA0021347A` as produced /// by .NET `new Guid("...").TryWriteBytes(span)`. First three groups /// are little-endian, last 8 bytes big-endian. Hand-computed: /// `Data1` = 0x99FCFEC4 → [C4 FE FC 99] /// `Data2` = 0x5260 → [60 52] /// `Data3` = 0x101B → [1B 10] /// `Data4` = BB CB 00 AA 00 21 34 7A (already BE) const IID_WIRE_BYTES: [u8; 16] = [ 0xC4, 0xFE, 0xFC, 0x99, 0x60, 0x52, 0x1B, 0x10, 0xBB, 0xCB, 0x00, 0xAA, 0x00, 0x21, 0x34, 0x7A, ]; #[test] fn iid_constant_matches_dotnet_wire_bytes() { assert_eq!(*IOBJECT_EXPORTER_IID.as_bytes(), IID_WIRE_BYTES); } #[test] fn iid_display_matches_dotnet_d_format() { // .NET `new Guid("99FCFEC4-5260-101B-BBCB-00AA0021347A").ToString("D")` // is lowercase `"99fcfec4-5260-101b-bbcb-00aa0021347a"`. assert_eq!( IOBJECT_EXPORTER_IID.to_string(), "99fcfec4-5260-101b-bbcb-00aa0021347a" ); } #[test] fn opnum_constants() { // Mirrors ObjectExporterMessages.cs:8-13. assert_eq!(RESOLVE_OXID_OPNUM, 0); assert_eq!(SIMPLE_PING_OPNUM, 1); assert_eq!(COMPLEX_PING_OPNUM, 2); assert_eq!(SERVER_ALIVE_OPNUM, 3); assert_eq!(RESOLVE_OXID2_OPNUM, 4); assert_eq!(SERVER_ALIVE2_OPNUM, 5); } #[test] fn protseq_constants() { // Mirrors ObjectExporterMessages.cs:15-16. assert_eq!(PROTSEQ_NCACN_IP_TCP, 0x0007); assert_eq!(PROTSEQ_NCALRPC, 0x001f); } #[test] fn align_helper_matches_dotnet() { // ObjectExporterMessages.cs:128-132. assert_eq!(align(0, 4), 0); assert_eq!(align(1, 4), 4); assert_eq!(align(3, 4), 4); assert_eq!(align(4, 4), 4); assert_eq!(align(5, 4), 8); assert_eq!(align(18, 4), 20); } #[test] fn encode_resolve_oxid_request_one_protseq() { // protseqs = [0x0007] -> body length = 8 + 2 + 2 + 4 + 2 = 18 → // aligned up to 20. let oxid = 0x1122_3344_5566_7788u64; let buf = encode_resolve_oxid_request(oxid, &[PROTSEQ_NCACN_IP_TCP]).unwrap(); assert_eq!(buf.len(), 20); // Layout asserts. assert_eq!(&buf[0..8], &oxid.to_le_bytes()); assert_eq!(&buf[8..10], &1u16.to_le_bytes()); // padding at 10..12 must be zero. assert_eq!(&buf[10..12], &[0u8, 0u8]); assert_eq!(&buf[12..16], &1u32.to_le_bytes()); assert_eq!(&buf[16..18], &PROTSEQ_NCACN_IP_TCP.to_le_bytes()); // 4-byte alignment padding at the tail. assert_eq!(&buf[18..20], &[0u8, 0u8]); } #[test] fn encode_resolve_oxid_request_two_protseqs() { // [0x0007, 0x001f] → 8 + 2 + 2 + 4 + 4 = 20 (already aligned). let buf = encode_resolve_oxid_request(0, &[PROTSEQ_NCACN_IP_TCP, PROTSEQ_NCALRPC]).unwrap(); assert_eq!(buf.len(), 20); assert_eq!(&buf[8..10], &2u16.to_le_bytes()); assert_eq!(&buf[12..16], &2u32.to_le_bytes()); assert_eq!(&buf[16..18], &PROTSEQ_NCACN_IP_TCP.to_le_bytes()); assert_eq!(&buf[18..20], &PROTSEQ_NCALRPC.to_le_bytes()); } #[test] fn encode_resolve_oxid_request_three_protseqs_aligned() { // [0x0007, 0x001f, 0x0007] → 8 + 2 + 2 + 4 + 6 = 22 → aligned to 24. let buf = encode_resolve_oxid_request( 0, &[PROTSEQ_NCACN_IP_TCP, PROTSEQ_NCALRPC, PROTSEQ_NCACN_IP_TCP], ) .unwrap(); assert_eq!(buf.len(), 24); assert_eq!(&buf[8..10], &3u16.to_le_bytes()); assert_eq!(&buf[12..16], &3u32.to_le_bytes()); assert_eq!(&buf[16..18], &PROTSEQ_NCACN_IP_TCP.to_le_bytes()); assert_eq!(&buf[18..20], &PROTSEQ_NCALRPC.to_le_bytes()); assert_eq!(&buf[20..22], &PROTSEQ_NCACN_IP_TCP.to_le_bytes()); // Trailing alignment padding. assert_eq!(&buf[22..24], &[0u8, 0u8]); } #[test] fn encode_resolve_oxid_request_empty_errors() { let err = encode_resolve_oxid_request(0, &[]).unwrap_err(); match err { RpcError::Decode { reason, .. } => { assert!(reason.contains("at least one protseq")); } other => panic!("expected RpcError::Decode, got {other:?}"), } } #[test] fn parse_resolve_oxid_failure_4_bytes() { // Single 4-byte status. let stub = 0x8000_4005u32.to_le_bytes(); let parsed = parse_resolve_oxid_failure(&stub).unwrap(); assert_eq!(parsed.error_status, 0x8000_4005); } #[test] fn parse_resolve_oxid_failure_12_bytes_takes_tail_4() { // Last 4 bytes only. let mut stub = vec![0u8; 12]; stub[8..12].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); let parsed = parse_resolve_oxid_failure(&stub).unwrap(); assert_eq!(parsed.error_status, 0xDEAD_BEEF); } #[test] fn parse_resolve_oxid_failure_short_buffer_errors() { let err = parse_resolve_oxid_failure(&[0u8; 3]).unwrap_err(); assert!(matches!( err, RpcError::ShortRead { expected: 4, actual: 3 } )); } /// Hand-build a success-shape `ResolveOxid` response stub with one /// `ncacn_ip_tcp` binding `"AB"` and a single `0x0000` security /// terminator. Returns `(stub, expected_ipid)`. fn build_success_stub() -> (Vec, Guid) { let mut buf = Vec::new(); // referent_id (non-zero). buf.extend_from_slice(&0x0000_0001u32.to_le_bytes()); // dual-string array u16 code units: // [0] tower_id = 0x0007 // [1] 'A' = 0x0041 // [2] 'B' = 0x0042 // [3] 0x0000 terminator // [4] 0x0000 security-binding terminator // entries = 5; security_offset = 4 (entry-start >= 4 are security). let entries: u16 = 5; let max_count: u32 = entries as u32; let security_offset: u16 = 4; buf.extend_from_slice(&max_count.to_le_bytes()); // offset 4..8 buf.extend_from_slice(&entries.to_le_bytes()); // offset 8..10 buf.extend_from_slice(&security_offset.to_le_bytes()); // offset 10..12 // dual-string array bytes (entries * 2 = 10 bytes; max_count * 2 = 10 — same here). for unit in [0x0007u16, b'A' as u16, b'B' as u16, 0x0000, 0x0000] { buf.extend_from_slice(&unit.to_le_bytes()); } // After 12 + 10 = 22 bytes, align to 4 → offset 24. Pad 2 bytes. assert_eq!(buf.len(), 22); buf.extend_from_slice(&[0u8, 0u8]); assert_eq!(buf.len(), 24); // Trailing 24 bytes: 16-byte IPID + 4-byte authn_hint + 4-byte status. let ipid_bytes: [u8; 16] = [ 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, ]; buf.extend_from_slice(&ipid_bytes); buf.extend_from_slice(&0x0000_000Au32.to_le_bytes()); // authn_hint buf.extend_from_slice(&0u32.to_le_bytes()); // status (buf, Guid::new(ipid_bytes)) } #[test] fn parse_resolve_oxid_result_happy_path() { let (stub, expected_ipid) = build_success_stub(); let parsed = parse_resolve_oxid_result(&stub).unwrap(); assert_eq!(parsed.rem_unknown_ipid, expected_ipid); assert_eq!(parsed.authn_hint, 0xA); assert_eq!(parsed.error_status, 0); // One ncacn_ip_tcp string-binding "AB". assert_eq!(parsed.bindings.len(), 1); let entry = &parsed.bindings[0]; assert_eq!(entry.tower_id, 0x0007); assert_eq!(entry.protocol, "ncacn_ip_tcp"); assert_eq!(entry.value, "AB"); // entry_start (0) < security_offset (4) → string binding. assert!(!entry.is_security_binding); } #[test] fn parse_resolve_oxid_result_referent_id_zero() { // referent_id = 0 → empty bindings, IPID zero, authn_hint 0, // status from the trailing 4 bytes (`:57-61`). let mut stub = vec![0u8; 32]; // referent_id zero (already). // Put the status at the end. stub[28..32].copy_from_slice(&0x8000_0001u32.to_le_bytes()); let parsed = parse_resolve_oxid_result(&stub).unwrap(); assert!(parsed.bindings.is_empty()); assert_eq!(parsed.rem_unknown_ipid, Guid::ZERO); assert_eq!(parsed.authn_hint, 0); assert_eq!(parsed.error_status, 0x8000_0001); } #[test] fn parse_resolve_oxid_result_max_count_lt_entries_errors() { let mut stub = vec![0u8; 64]; stub[0..4].copy_from_slice(&1u32.to_le_bytes()); // referent_id != 0 stub[4..8].copy_from_slice(&1u32.to_le_bytes()); // max_count = 1 stub[8..10].copy_from_slice(&5u16.to_le_bytes()); // entries = 5 stub[10..12].copy_from_slice(&0u16.to_le_bytes()); let err = parse_resolve_oxid_result(&stub).unwrap_err(); match err { RpcError::Decode { reason, .. } => { assert!(reason.contains("max count")); } other => panic!("expected RpcError::Decode, got {other:?}"), } } #[test] fn parse_resolve_oxid_result_truncated_trailing_errors() { // Build a valid header but drop the trailing 24 bytes. The // dual-string array is empty (entries=0, max_count=0), so offset // after alignment = 12 + 0 = 12. Buffer length 32 leaves only 20 // bytes of trailing space — but the parser needs 24, so it must // error. let mut stub = vec![0u8; 32]; stub[0..4].copy_from_slice(&1u32.to_le_bytes()); // referent_id != 0 stub[4..8].copy_from_slice(&0u32.to_le_bytes()); // max_count = 0 stub[8..10].copy_from_slice(&0u16.to_le_bytes()); // entries = 0 stub[10..12].copy_from_slice(&0u16.to_le_bytes()); // security_offset = 0 let err = parse_resolve_oxid_result(&stub).unwrap_err(); match err { RpcError::Decode { reason, .. } => { assert!(reason.contains("trailing fields are truncated")); } other => panic!("expected RpcError::Decode, got {other:?}"), } } #[test] fn parse_resolve_oxid_result_short_buffer_errors() { let err = parse_resolve_oxid_result(&[0u8; 31]).unwrap_err(); assert!(matches!( err, RpcError::ShortRead { expected: 32, actual: 31 } )); } #[test] fn decode_dual_string_array_question_mark_escape() { // `?` (not ``) is the non-printable escape per // ObjectExporterMessages.cs:115. Build: // [0] tower 0x0007 // [1] 0x0100 (non-printable) // [2] 'a' (printable) // [3] 0x0000 terminator // entries = 4, security_offset = 4 → no security binding. let mut data = Vec::new(); for unit in [0x0007u16, 0x0100, b'a' as u16, 0x0000] { data.extend_from_slice(&unit.to_le_bytes()); } let decoded = decode_dual_string_array(&data, 4, 4); assert_eq!(decoded.len(), 1); assert_eq!(decoded[0].tower_id, 0x0007); assert_eq!(decoded[0].protocol, "ncacn_ip_tcp"); // `?` escape (single character), not `<0100>`. assert_eq!(decoded[0].value, "?a"); assert!(!decoded[0].is_security_binding); } #[test] fn decode_dual_string_array_unknown_protseq_label() { // Tower 0x0009 (ncacn_np in ComObjRef) gets the // `protseq_0x0009` fallback here, **not** the table lookup. let mut data = Vec::new(); for unit in [0x0009u16, b'X' as u16, 0x0000] { data.extend_from_slice(&unit.to_le_bytes()); } let decoded = decode_dual_string_array(&data, 3, 3); assert_eq!(decoded.len(), 1); assert_eq!(decoded[0].protocol, "protseq_0x0009"); assert_eq!(decoded[0].value, "X"); } #[test] fn decode_dual_string_array_security_offset_split() { // Two entries, each `tower=0x0007 / value / 0x0000`, total 6 u16 // code units. security_offset = 3 means the second entry (start // index 3) is a security binding. let mut data = Vec::new(); for unit in [0x0007u16, b'A' as u16, 0x0000, 0x0007, b'B' as u16, 0x0000] { data.extend_from_slice(&unit.to_le_bytes()); } let decoded = decode_dual_string_array(&data, 6, 3); assert_eq!(decoded.len(), 2); assert_eq!(decoded[0].value, "A"); assert!(!decoded[0].is_security_binding); assert_eq!(decoded[1].value, "B"); assert!(decoded[1].is_security_binding); } }