30138629d3
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>
750 lines
28 KiB
Rust
750 lines
28 KiB
Rust
//! `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 `<XXXX>` 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 <padding> 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<Vec<u8>, 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::<u16>();
|
|
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<ComDualStringEntry>,
|
|
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<ResolveOxidFailure, RpcError> {
|
|
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<ResolveOxidResult, RpcError> {
|
|
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
|
|
/// `<XXXX>` (`: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<ComDualStringEntry> {
|
|
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<u8>, 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 `<XXXX>`) 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);
|
|
}
|
|
}
|