ecbf282f6d
Lands the codec-only prerequisites for M2 wave 3 (callback exporter). The TCP server itself (port of ManagedCallbackExporter.cs's TcpListener + accept loop) follows next iteration in the mxaccess-callback crate. New modules - nmx_metadata.rs (9 tests) — port of NmxProcedureMetadata.cs. INmxService2 + INmxSvcCallback IIDs, NdrProcedureDescriptor with per-opnum metadata for the 9 INmxService2 procedures (opnums 3..11) and 2 INmxSvcCallback procedures (opnums 3, 4). - nmx_callback_messages.rs (8 tests) — port of NmxSvcCallbackMessages.cs. parse_callback_request decodes OrpcThis + i32 size + i32 max_count + body bytes; encode_callback_response produces the 12-byte OrpcThat + HRESULT response. objref.rs additions - ComObjRefBuilder::create_standard_objref (8 tests) — port of the second class in ManagedCallbackExporter.cs:337-393. Pure-Rust OBJREF emitter that builds 68-byte header + dual-string array. Note this is *not* the Win32 CoMarshalInterface-based ComObjRefProvider.cs (still open as F6); it's the higher-level emitter the callback exporter uses to build OBJREF bytes from primitives. - CALLBACK_OBJREF_AUTH_SERVICES const exposes the 7-entry auth-service tower-id table (NTLM SSP through Kerberos extension) the .NET reference advertises in every callback OBJREF. Test count delta: 319 -> 344 (+25; mxaccess-rpc 102 -> 127, codec unchanged at 215, parity unchanged at 2). All four DoD gates green. Open followups touched: none new; F6 advances toward resolution but the windows-rs Win32 wrapper part stays open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
916 lines
37 KiB
Rust
916 lines
37 KiB
Rust
//! `ComObjRef` — DCOM OBJREF parser.
|
|
//!
|
|
//! Direct port of `src/MxNativeClient/ComObjRef.cs`. Parses the marshalled
|
|
//! interface byte stream produced by `CoMarshalInterface` (per `[MS-DCOM]`
|
|
//! §2.2.18) into a structured form the RPC layer can inspect for OXID/OID/IPID
|
|
//! and the dual-string-array bindings (string + security towers).
|
|
//!
|
|
//! The .NET reference parses 68 fixed bytes followed by the dual-string array,
|
|
//! which is decoded character-by-character and used purely for diagnostics.
|
|
//! The Rust port mirrors that parser shape exactly — every field offset,
|
|
//! the `Math.Min(entries, data.Length / 2)` bound, and the printable-ASCII
|
|
//! escaping of each UTF-16 code unit are 1:1 with `ComObjRef.cs:18-117`.
|
|
//!
|
|
//! `ComObjRefProvider.cs` is **not** ported here — it is a thin wrapper around
|
|
//! Win32 `CoMarshalInterface` / `IStream` / `GlobalLock` and produces OBJREF
|
|
//! bytes by calling into ole32. That belongs behind `windows-rs` in a later
|
|
//! M2/M3 wave; the pure-Rust parser stands alone and is what M2 wave 1 needs
|
|
//! for OBJREF inspection on inbound activation responses. See followup F1
|
|
//! in this module's report.
|
|
|
|
// 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`.
|
|
#![allow(clippy::indexing_slicing)]
|
|
|
|
use std::fmt::Write as _;
|
|
|
|
// `Guid` and `RpcError` are crate-shared since M2 wave 2 — see
|
|
// `design/followups.md` F7+F8.
|
|
pub use crate::error::RpcError;
|
|
pub use crate::guid::Guid;
|
|
|
|
/// Encoded layout per `ComObjRef.cs:25-39`:
|
|
///
|
|
/// ```text
|
|
/// offset size field
|
|
/// 0 4 signature u32 LE = 0x574F454D ("MEOW")
|
|
/// 4 4 flags u32 LE (1 = OBJREF_STANDARD; only standard parsed)
|
|
/// 8 16 iid GUID
|
|
/// 24 4 std_flags u32 LE (STDOBJREF flags)
|
|
/// 28 4 public_refs u32 LE
|
|
/// 32 8 oxid u64 LE
|
|
/// 40 8 oid u64 LE
|
|
/// 48 16 ipid GUID
|
|
/// 64 2 dual_string_entries u16 LE (count of u16 code units in the array)
|
|
/// 66 2 dual_string_security_offset u16 LE (boundary index between string and security bindings)
|
|
/// 68 .. dual-string array (variable; UTF-16LE code units terminated by 0x0000 per entry)
|
|
/// ```
|
|
///
|
|
/// **Header length** is 68 bytes; the dual-string array follows starting at
|
|
/// offset 68. The `dual_string_entries` count is bounded by the actual byte
|
|
/// length of the trailing bytes via `min(entries, data.len() / 2)`
|
|
/// (`ComObjRef.cs:59`).
|
|
pub const OBJREF_HEADER_LEN: usize = 68;
|
|
|
|
/// "MEOW" — the OBJREF signature (`ComObjRef.cs:29`, also `[MS-DCOM]` §2.2.18.1).
|
|
pub const OBJREF_SIGNATURE: u32 = 0x574F_454D;
|
|
|
|
const FLAGS_OFFSET: usize = 4;
|
|
const IID_OFFSET: usize = 8;
|
|
const STD_FLAGS_OFFSET: usize = 24;
|
|
const PUBLIC_REFS_OFFSET: usize = 28;
|
|
const OXID_OFFSET: usize = 32;
|
|
const OID_OFFSET: usize = 40;
|
|
const IPID_OFFSET: usize = 48;
|
|
const DUAL_STRING_ENTRIES_OFFSET: usize = 64;
|
|
const DUAL_STRING_SECURITY_OFFSET_OFFSET: usize = 66;
|
|
|
|
/// One decoded entry of the OBJREF dual-string array. `value` is the
|
|
/// printable-ASCII escaping of the UTF-16 string per `ComObjRef.cs:82-91` —
|
|
/// non-printable code units appear as `<XXXX>` lowercase hex. `is_security_binding`
|
|
/// is set when the entry's start offset (in u16 units) is at or past
|
|
/// `DualStringSecurityOffset`.
|
|
///
|
|
/// Mirrors `ComDualStringEntry` (`ComObjRef.cs:138-145`).
|
|
///
|
|
/// `protocol` is `Cow<'static, str>` because the OBJREF parser uses the
|
|
/// 7-entry static table (`Cow::Borrowed`) while the M2 wave 2 OXID-resolve
|
|
/// parser uses `format!("protseq_0x{:04x}", tower_id)` (`Cow::Owned`) for
|
|
/// unknown tower ids (`ObjectExporterMessages.cs:120`). The two parsers
|
|
/// share the entry type but emit different protocol labels for the same
|
|
/// tower id — this is intentional and matches the .NET reference.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct ComDualStringEntry {
|
|
pub tower_id: u16,
|
|
pub protocol: std::borrow::Cow<'static, str>,
|
|
pub value: String,
|
|
pub is_security_binding: bool,
|
|
}
|
|
|
|
impl ComDualStringEntry {
|
|
/// Mirrors `ComDualStringEntry.ToDiagnosticString` (`ComObjRef.cs:140-144`):
|
|
/// `"<kind>:0x<tower_id_lc>:<protocol>:<value>"`.
|
|
pub fn to_diagnostic_string(&self) -> String {
|
|
let kind = if self.is_security_binding {
|
|
"security"
|
|
} else {
|
|
"string"
|
|
};
|
|
format!(
|
|
"{}:0x{:04x}:{}:{}",
|
|
kind, self.tower_id, self.protocol, self.value
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Parsed DCOM OBJREF (standard form).
|
|
///
|
|
/// Mirrors `ComObjRef` record (`ComObjRef.cs:5-16`). All eleven fields of the
|
|
/// .NET record are preserved including `signature`, `flags`, `std_flags`,
|
|
/// `dual_string_entries`, and `dual_string_security_offset` — even though the
|
|
/// signature is a known constant, the parser does not validate it (the .NET
|
|
/// reference doesn't either; bytes are surfaced verbatim per CLAUDE.md
|
|
/// preserve-unknown-bytes rule).
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct ComObjRef {
|
|
pub signature: u32,
|
|
pub flags: u32,
|
|
pub iid: Guid,
|
|
pub standard_flags: u32,
|
|
pub public_refs: u32,
|
|
pub oxid: u64,
|
|
pub oid: u64,
|
|
pub ipid: Guid,
|
|
/// Raw entry-count field — measured in **u16 code units**, not entries.
|
|
/// Preserved verbatim from the wire even when it overruns the buffer; the
|
|
/// parse loop bounds itself by `min(entries, data.len() / 2)`
|
|
/// (`ComObjRef.cs:59`).
|
|
pub dual_string_entries: u16,
|
|
/// Boundary (in u16 code-unit indices) between string bindings and
|
|
/// security bindings within the dual-string array (`ComObjRef.cs:98`).
|
|
pub dual_string_security_offset: u16,
|
|
pub dual_string_entries_decoded: Vec<ComDualStringEntry>,
|
|
}
|
|
|
|
impl ComObjRef {
|
|
/// Header length (68 bytes) before the dual-string array.
|
|
pub const HEADER_LEN: usize = OBJREF_HEADER_LEN;
|
|
|
|
/// Parse an OBJREF buffer. Mirrors `ComObjRef.Parse` (`ComObjRef.cs:18-40`)
|
|
/// byte-for-byte: 68-byte fixed header followed by a UTF-16LE
|
|
/// dual-string array bounded by `min(entries, tail.len() / 2)`.
|
|
///
|
|
/// The signature field is read but not validated — the .NET reference
|
|
/// surfaces it verbatim so callers can diff against captures.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// - [`RpcError::ShortRead`] if `buffer.len() < 68`
|
|
/// (matches `ComObjRef.cs:20-23`).
|
|
pub fn parse(buffer: &[u8]) -> Result<Self, RpcError> {
|
|
if buffer.len() < Self::HEADER_LEN {
|
|
return Err(RpcError::ShortRead {
|
|
expected: Self::HEADER_LEN,
|
|
actual: buffer.len(),
|
|
});
|
|
}
|
|
|
|
let dual_string_entries = read_u16_le(buffer, DUAL_STRING_ENTRIES_OFFSET);
|
|
let security_offset = read_u16_le(buffer, DUAL_STRING_SECURITY_OFFSET_OFFSET);
|
|
|
|
let mut iid_bytes = [0u8; 16];
|
|
iid_bytes.copy_from_slice(&buffer[IID_OFFSET..IID_OFFSET + 16]);
|
|
let mut ipid_bytes = [0u8; 16];
|
|
ipid_bytes.copy_from_slice(&buffer[IPID_OFFSET..IPID_OFFSET + 16]);
|
|
|
|
let tail = &buffer[Self::HEADER_LEN..];
|
|
let decoded = decode_dual_string_array(tail, dual_string_entries, security_offset);
|
|
|
|
Ok(Self {
|
|
signature: read_u32_le(buffer, 0),
|
|
flags: read_u32_le(buffer, FLAGS_OFFSET),
|
|
iid: Guid(iid_bytes),
|
|
standard_flags: read_u32_le(buffer, STD_FLAGS_OFFSET),
|
|
public_refs: read_u32_le(buffer, PUBLIC_REFS_OFFSET),
|
|
oxid: read_u64_le(buffer, OXID_OFFSET),
|
|
oid: read_u64_le(buffer, OID_OFFSET),
|
|
ipid: Guid(ipid_bytes),
|
|
dual_string_entries,
|
|
dual_string_security_offset: security_offset,
|
|
dual_string_entries_decoded: decoded,
|
|
})
|
|
}
|
|
|
|
/// Diagnostic line emitter — byte-identical to `ToDiagnosticLines`
|
|
/// (`ComObjRef.cs:42-55`). The output is intended for matching against
|
|
/// Frida-captured probe output (`captures/057-managed-callback-route-service-trace-saved/probe.stdout.txt`).
|
|
pub fn to_diagnostic_lines(&self) -> Vec<String> {
|
|
let dual_strings = self
|
|
.dual_string_entries_decoded
|
|
.iter()
|
|
.map(ComDualStringEntry::to_diagnostic_string)
|
|
.collect::<Vec<_>>()
|
|
.join("|");
|
|
vec![
|
|
format!("objref_signature=0x{:08X}", self.signature),
|
|
format!("objref_flags=0x{:08X}", self.flags),
|
|
format!("objref_iid={}", self.iid),
|
|
format!("std_flags=0x{:08X}", self.standard_flags),
|
|
format!("std_public_refs={}", self.public_refs),
|
|
format!("std_oxid=0x{:016X}", self.oxid),
|
|
format!("std_oid=0x{:016X}", self.oid),
|
|
format!("std_ipid={}", self.ipid),
|
|
format!("dual_string_entries={}", self.dual_string_entries),
|
|
format!(
|
|
"dual_string_security_offset={}",
|
|
self.dual_string_security_offset
|
|
),
|
|
format!("dual_strings={}", dual_strings),
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Decode the trailing dual-string array. Mirrors
|
|
/// `DecodeDualStringArray` (`ComObjRef.cs:57-102`).
|
|
///
|
|
/// The loop walks `i` in u16 code-unit indices, capped by
|
|
/// `min(entries, data.len() / 2)`. Each entry begins with a 16-bit
|
|
/// `tower_id`; if zero, it terminates the string-binding region (the
|
|
/// `continue` skips to the next index without producing an entry — same as
|
|
/// the .NET source). Otherwise the following u16 code units up to (but not
|
|
/// including) the next 0x0000 terminator form the entry's value, escaped
|
|
/// printable-ASCII per the `0x20..=0x7e` rule.
|
|
fn decode_dual_string_array(
|
|
data: &[u8],
|
|
entries: u16,
|
|
security_offset: u16,
|
|
) -> Vec<ComDualStringEntry> {
|
|
let entries = entries as usize;
|
|
let count = entries.min(data.len() / 2);
|
|
let mut strings = Vec::new();
|
|
|
|
let mut i: usize = 0;
|
|
while i < count {
|
|
let entry_start = i;
|
|
let tower_id = read_u16_le(data, i * 2);
|
|
i += 1;
|
|
if tower_id == 0 {
|
|
continue;
|
|
}
|
|
|
|
let mut text = String::new();
|
|
while i < count {
|
|
let value = read_u16_le(data, i * 2);
|
|
i += 1;
|
|
if value == 0 {
|
|
break;
|
|
}
|
|
|
|
if (0x20..=0x7e).contains(&value) {
|
|
// Safe: 0x20..=0x7e is printable ASCII, valid UTF-8.
|
|
text.push(value as u8 as char);
|
|
} else {
|
|
// Non-printable: emit "<XXXX>" lowercase hex (mirrors .NET
|
|
// `value.ToString("x4", InvariantCulture)`).
|
|
// write! to a String never fails; ignore the Result.
|
|
let _ = write!(&mut text, "<{:04x}>", value);
|
|
}
|
|
}
|
|
|
|
strings.push(ComDualStringEntry {
|
|
tower_id,
|
|
protocol: std::borrow::Cow::Borrowed(protocol_tower_name(tower_id)),
|
|
value: text,
|
|
is_security_binding: entry_start >= security_offset as usize,
|
|
});
|
|
}
|
|
|
|
strings
|
|
}
|
|
|
|
/// Protocol-tower name table per `ComObjRef.cs:104-117`. Returns `"unknown"`
|
|
/// for unrecognised tower ids — mirrors the `_ =>` fall-through.
|
|
pub const fn protocol_tower_name(tower_id: u16) -> &'static str {
|
|
match tower_id {
|
|
0x0007 => "ncacn_ip_tcp",
|
|
0x0008 => "ncadg_ip_udp",
|
|
0x0009 => "ncacn_np",
|
|
0x000f => "ncacn_spx",
|
|
0x0010 => "ncacn_nb_nb",
|
|
0x0016 => "ncadg_ip_udp_or_netbios",
|
|
0x001f => "ncalrpc",
|
|
_ => "unknown",
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn read_u16_le(bytes: &[u8], offset: usize) -> u16 {
|
|
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
|
|
}
|
|
|
|
#[inline]
|
|
fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
|
|
u32::from_le_bytes([
|
|
bytes[offset],
|
|
bytes[offset + 1],
|
|
bytes[offset + 2],
|
|
bytes[offset + 3],
|
|
])
|
|
}
|
|
|
|
#[inline]
|
|
fn read_u64_le(bytes: &[u8], offset: usize) -> u64 {
|
|
u64::from_le_bytes([
|
|
bytes[offset],
|
|
bytes[offset + 1],
|
|
bytes[offset + 2],
|
|
bytes[offset + 3],
|
|
bytes[offset + 4],
|
|
bytes[offset + 5],
|
|
bytes[offset + 6],
|
|
bytes[offset + 7],
|
|
])
|
|
}
|
|
|
|
// Compile-time invariant: header length matches the documented byte layout.
|
|
const _: () = assert!(OBJREF_HEADER_LEN == 68);
|
|
const _: () = assert!(OBJREF_SIGNATURE == 0x574F_454D);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ComObjRefBuilder — pure-Rust OBJREF emitter.
|
|
// Direct port of the second class in
|
|
// `src/MxNativeClient/ManagedCallbackExporter.cs:337-393`.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Auth-service tower IDs the .NET reference advertises in every callback
|
|
/// OBJREF. Mirrors the hard-coded array at
|
|
/// `ManagedCallbackExporter.cs:362`. Each id appears in the security-binding
|
|
/// portion of the dual-string array followed by `0xFFFF` and a terminator.
|
|
///
|
|
/// IDs in order: NTLM SSP (0x0009), GSS Negotiate (0x001E), Kerberos (0x0010),
|
|
/// SSL/TLS (0x000A), Schannel (0x0016), DPA (0x001F), Kerberos extension
|
|
/// (0x000E). The Rust port carries the same set verbatim — no synthesis.
|
|
pub const CALLBACK_OBJREF_AUTH_SERVICES: [u16; 7] =
|
|
[0x0009, 0x001E, 0x0010, 0x000A, 0x0016, 0x001F, 0x000E];
|
|
|
|
/// Builds standard OBJREF byte buffers for the callback exporter to publish.
|
|
///
|
|
/// Mirrors the static `ComObjRefBuilder` class
|
|
/// (`src/MxNativeClient/ManagedCallbackExporter.cs:337-393`). The .NET reference
|
|
/// only ever emits *standard* OBJREFs (`flags = 1`); the Rust port matches.
|
|
///
|
|
/// This is the higher-level emitter that builds OBJREF bytes from primitives.
|
|
/// It is **not** the Win32 `CoMarshalInterface`-based emitter from
|
|
/// `ComObjRefProvider.cs` — that wrapper around `ole32` is still tracked as
|
|
/// open follow-up F6 (it requires `windows-rs` and the M2 wave 3 callback
|
|
/// exporter to register the emitted OBJREF with COM).
|
|
pub struct ComObjRefBuilder;
|
|
|
|
impl ComObjRefBuilder {
|
|
/// Build a standard-OBJREF buffer for a given IID, OXID/OID/IPID, and one
|
|
/// or more `ncacn_ip_tcp` string bindings (e.g. `"hostname[5985]"`).
|
|
/// Mirrors `ComObjRefBuilder.CreateStandardObjRef`
|
|
/// (`ManagedCallbackExporter.cs:339-392`).
|
|
///
|
|
/// # Layout (`cs:348-389`)
|
|
///
|
|
/// ```text
|
|
/// offset size field
|
|
/// 0 4 signature u32 LE = 0x574F454D ("MEOW")
|
|
/// 4 4 flags u32 LE = 1
|
|
/// 8 16 iid GUID
|
|
/// 24 4 std_flags u32 LE
|
|
/// 28 4 public_refs u32 LE
|
|
/// 32 8 oxid u64 LE
|
|
/// 40 8 oid u64 LE
|
|
/// 48 16 ipid GUID
|
|
/// 64 2 entries u16 LE (count of u16 code units below)
|
|
/// 66 2 security_offset u16 LE (in u16 code units)
|
|
/// 68 .. dual-string array (variable-length u16 LE words)
|
|
/// ```
|
|
///
|
|
/// # `entries` and `security_offset`
|
|
///
|
|
/// `entries` is the **total u16-code-unit count** of the dual-string
|
|
/// array (string bindings + 0 separator + 7 security entries + final 0).
|
|
/// `security_offset` is the index (in u16 units) where security bindings
|
|
/// begin — `cs:348` computes this as
|
|
/// `sum(1 + binding.len() + 1 for binding in stringBindings) + 1`, i.e.
|
|
/// per-binding `tower_id` (1 word) + `binding.len()` ASCII chars (one
|
|
/// word each) + null terminator (1 word), plus the trailing 0 separator
|
|
/// that ends the string section.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Never panics. All length math saturates: bindings longer than
|
|
/// `u16::MAX - HEADER_LEN/2 - SECURITY_TAIL_LEN` are not representable
|
|
/// in the 16-bit `entries` field, and the .NET reference does not guard
|
|
/// against this either; callers are expected to keep bindings short
|
|
/// (typical `hostname[port]` is < 100 chars).
|
|
#[must_use]
|
|
pub fn create_standard_objref(
|
|
iid: Guid,
|
|
std_flags: u32,
|
|
public_refs: u32,
|
|
oxid: u64,
|
|
oid: u64,
|
|
ipid: Guid,
|
|
string_bindings: &[&str],
|
|
) -> Vec<u8> {
|
|
// security_offset = sum_{b in string_bindings}(1 + b.len() + 1) + 1
|
|
// (cs:348). u16-truncating cast mirrors `(ushort)`.
|
|
let security_offset: u16 = string_bindings
|
|
.iter()
|
|
.map(|b| 1 + b.len() + 1)
|
|
.sum::<usize>()
|
|
.saturating_add(1)
|
|
.min(u16::MAX as usize) as u16;
|
|
|
|
// Build the u16 word array.
|
|
let mut words: Vec<u16> = Vec::new();
|
|
|
|
// String-bindings section: per binding, [0x0007 (ncacn_ip_tcp), each
|
|
// ASCII char as u16, terminator 0] (cs:350-359).
|
|
for binding in string_bindings {
|
|
words.push(0x0007);
|
|
for ch in binding.chars() {
|
|
words.push(ch as u16);
|
|
}
|
|
words.push(0);
|
|
}
|
|
// 0 separator that ends the string section (cs:361).
|
|
words.push(0);
|
|
|
|
// Security-bindings section: 7 hard-coded tower entries, each
|
|
// [tower_id, 0xFFFF, 0] (cs:362-367).
|
|
for &auth in &CALLBACK_OBJREF_AUTH_SERVICES {
|
|
words.push(auth);
|
|
words.push(0xFFFF);
|
|
words.push(0);
|
|
}
|
|
|
|
// Final terminator (cs:369).
|
|
words.push(0);
|
|
|
|
// u16-truncating cast mirrors `(ushort)words.Count` (cs:371).
|
|
let entries: u16 = words.len().min(u16::MAX as usize) as u16;
|
|
let mut buffer = vec![0u8; OBJREF_HEADER_LEN + words.len() * 2];
|
|
|
|
// Fixed 68-byte header (cs:373-382).
|
|
buffer[0..4].copy_from_slice(&OBJREF_SIGNATURE.to_le_bytes());
|
|
buffer[4..8].copy_from_slice(&1u32.to_le_bytes()); // flags = 1 (OBJREF_STANDARD)
|
|
buffer[8..24].copy_from_slice(iid.as_bytes());
|
|
buffer[24..28].copy_from_slice(&std_flags.to_le_bytes());
|
|
buffer[28..32].copy_from_slice(&public_refs.to_le_bytes());
|
|
buffer[32..40].copy_from_slice(&oxid.to_le_bytes());
|
|
buffer[40..48].copy_from_slice(&oid.to_le_bytes());
|
|
buffer[48..64].copy_from_slice(ipid.as_bytes());
|
|
buffer[64..66].copy_from_slice(&entries.to_le_bytes());
|
|
buffer[66..68].copy_from_slice(&security_offset.to_le_bytes());
|
|
|
|
// Dual-string array body (cs:384-389).
|
|
let mut offset = OBJREF_HEADER_LEN;
|
|
for word in &words {
|
|
buffer[offset..offset + 2].copy_from_slice(&word.to_le_bytes());
|
|
offset += 2;
|
|
}
|
|
|
|
buffer
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(
|
|
clippy::unwrap_used,
|
|
clippy::expect_used,
|
|
clippy::indexing_slicing,
|
|
clippy::panic
|
|
)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Hand-built OBJREF: signature + flags=1 + sample IID + std_flags +
|
|
/// public_refs=5 + fake OXID/OID/IPID + dual_string array containing one
|
|
/// `ncacn_ip_tcp` entry then a `0x0000` terminator. Returns the bytes.
|
|
fn build_minimal_objref() -> Vec<u8> {
|
|
let mut buf = Vec::new();
|
|
|
|
// signature "MEOW" 0x574F454D LE
|
|
buf.extend_from_slice(&0x574F_454Du32.to_le_bytes());
|
|
// flags = 1 (OBJREF_STANDARD)
|
|
buf.extend_from_slice(&1u32.to_le_bytes());
|
|
// iid (16 bytes; arbitrary)
|
|
buf.extend_from_slice(&[
|
|
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
|
0x0f, 0x10,
|
|
]);
|
|
// std_flags
|
|
buf.extend_from_slice(&0u32.to_le_bytes());
|
|
// public_refs = 5
|
|
buf.extend_from_slice(&5u32.to_le_bytes());
|
|
// oxid
|
|
buf.extend_from_slice(&0x1122_3344_5566_7788u64.to_le_bytes());
|
|
// oid
|
|
buf.extend_from_slice(&0xAABB_CCDD_EEFF_0011u64.to_le_bytes());
|
|
// ipid
|
|
buf.extend_from_slice(&[
|
|
0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad,
|
|
0xae, 0xaf,
|
|
]);
|
|
|
|
// Build the dual-string array:
|
|
// tower_id 0x0007 (ncacn_ip_tcp)
|
|
// "AB" as UTF-16LE
|
|
// 0x0000 terminator
|
|
// That's 4 u16 code units = 8 bytes.
|
|
let array_units: [u16; 4] = [0x0007, b'A' as u16, b'B' as u16, 0x0000];
|
|
let dual_entries: u16 = array_units.len() as u16;
|
|
let security_offset: u16 = dual_entries; // no security bindings
|
|
|
|
// dual_string_entries (count of u16 code units)
|
|
buf.extend_from_slice(&dual_entries.to_le_bytes());
|
|
// dual_string_security_offset
|
|
buf.extend_from_slice(&security_offset.to_le_bytes());
|
|
|
|
// header now exactly 68 bytes
|
|
assert_eq!(buf.len(), 68);
|
|
|
|
for unit in array_units {
|
|
buf.extend_from_slice(&unit.to_le_bytes());
|
|
}
|
|
buf
|
|
}
|
|
|
|
#[test]
|
|
fn parse_minimal_objref() {
|
|
let bytes = build_minimal_objref();
|
|
let parsed = ComObjRef::parse(&bytes).unwrap();
|
|
|
|
assert_eq!(parsed.signature, 0x574F_454D);
|
|
assert_eq!(parsed.flags, 1);
|
|
assert_eq!(parsed.standard_flags, 0);
|
|
assert_eq!(parsed.public_refs, 5);
|
|
assert_eq!(parsed.oxid, 0x1122_3344_5566_7788);
|
|
assert_eq!(parsed.oid, 0xAABB_CCDD_EEFF_0011);
|
|
assert_eq!(parsed.dual_string_entries, 4);
|
|
assert_eq!(parsed.dual_string_security_offset, 4);
|
|
assert_eq!(parsed.dual_string_entries_decoded.len(), 1);
|
|
|
|
let entry = &parsed.dual_string_entries_decoded[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 diagnostic_lines_format_minimal() {
|
|
let bytes = build_minimal_objref();
|
|
let parsed = ComObjRef::parse(&bytes).unwrap();
|
|
let lines = parsed.to_diagnostic_lines();
|
|
// Per ComObjRef.cs:42-55 there are exactly 11 lines.
|
|
assert_eq!(lines.len(), 11);
|
|
assert_eq!(lines[0], "objref_signature=0x574F454D");
|
|
assert_eq!(lines[1], "objref_flags=0x00000001");
|
|
assert_eq!(lines[3], "std_flags=0x00000000");
|
|
assert_eq!(lines[4], "std_public_refs=5");
|
|
assert_eq!(lines[5], "std_oxid=0x1122334455667788");
|
|
assert_eq!(lines[6], "std_oid=0xAABBCCDDEEFF0011");
|
|
assert_eq!(lines[8], "dual_string_entries=4");
|
|
assert_eq!(lines[9], "dual_string_security_offset=4");
|
|
assert_eq!(lines[10], "dual_strings=string:0x0007:ncacn_ip_tcp:AB");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_rejects_short_buffer() {
|
|
// 67-byte buffer (one shy of header) must error, not panic.
|
|
let err = ComObjRef::parse(&[0u8; 67]).unwrap_err();
|
|
match err {
|
|
RpcError::ShortRead { expected, actual } => {
|
|
assert_eq!(expected, 68);
|
|
assert_eq!(actual, 67);
|
|
}
|
|
other => panic!("expected ShortRead, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_accepts_exact_header_no_array() {
|
|
// 68 bytes with dual_string_entries=0 → no decoded entries.
|
|
let mut buf = vec![0u8; 68];
|
|
// signature
|
|
buf[0..4].copy_from_slice(&0x574F_454Du32.to_le_bytes());
|
|
let parsed = ComObjRef::parse(&buf).unwrap();
|
|
assert_eq!(parsed.dual_string_entries, 0);
|
|
assert_eq!(parsed.dual_string_security_offset, 0);
|
|
assert!(parsed.dual_string_entries_decoded.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_tower_name_table() {
|
|
// All 7 documented tower ids per ComObjRef.cs:106-117.
|
|
assert_eq!(protocol_tower_name(0x0007), "ncacn_ip_tcp");
|
|
assert_eq!(protocol_tower_name(0x0008), "ncadg_ip_udp");
|
|
assert_eq!(protocol_tower_name(0x0009), "ncacn_np");
|
|
assert_eq!(protocol_tower_name(0x000f), "ncacn_spx");
|
|
assert_eq!(protocol_tower_name(0x0010), "ncacn_nb_nb");
|
|
assert_eq!(protocol_tower_name(0x0016), "ncadg_ip_udp_or_netbios");
|
|
assert_eq!(protocol_tower_name(0x001f), "ncalrpc");
|
|
// Fall-through.
|
|
assert_eq!(protocol_tower_name(0x0000), "unknown");
|
|
assert_eq!(protocol_tower_name(0xFFFF), "unknown");
|
|
}
|
|
|
|
#[test]
|
|
fn dual_string_array_overrun_bounded() {
|
|
// Build a header that claims 1000 dual-string code units but only
|
|
// includes 4 bytes (= 2 code units) of trailing data. The parser
|
|
// must bound itself via min(entries, data.len()/2) per
|
|
// ComObjRef.cs:59 and not read past the end.
|
|
let mut buf = build_minimal_objref();
|
|
// Truncate the trailing dual-string bytes back to 0 and lie about
|
|
// entries=1000.
|
|
buf.truncate(68);
|
|
buf[DUAL_STRING_ENTRIES_OFFSET..DUAL_STRING_ENTRIES_OFFSET + 2]
|
|
.copy_from_slice(&1000u16.to_le_bytes());
|
|
let parsed = ComObjRef::parse(&buf).unwrap();
|
|
// The wire-declared entry count is preserved verbatim per
|
|
// CLAUDE.md unknown-bytes rule.
|
|
assert_eq!(parsed.dual_string_entries, 1000);
|
|
// But the loop bound prevents any decoding.
|
|
assert!(parsed.dual_string_entries_decoded.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn security_binding_flag_split() {
|
|
// Build a dual-string array with one string binding then one security
|
|
// binding. Layout (u16 code units):
|
|
// [0] tower=0x0007 (string binding starts at index 0)
|
|
// [1] 'A'
|
|
// [2] 0x0000 terminator
|
|
// [3] tower=0x0007 (security binding starts at index 3)
|
|
// [4] 'B'
|
|
// [5] 0x0000 terminator
|
|
// dual_string_entries = 6, security_offset = 3 (entries with start
|
|
// index >= 3 are security bindings).
|
|
let mut buf = vec![0u8; 68];
|
|
buf[0..4].copy_from_slice(&0x574F_454Du32.to_le_bytes());
|
|
let entries: u16 = 6;
|
|
let sec_off: u16 = 3;
|
|
buf[DUAL_STRING_ENTRIES_OFFSET..DUAL_STRING_ENTRIES_OFFSET + 2]
|
|
.copy_from_slice(&entries.to_le_bytes());
|
|
buf[DUAL_STRING_SECURITY_OFFSET_OFFSET..DUAL_STRING_SECURITY_OFFSET_OFFSET + 2]
|
|
.copy_from_slice(&sec_off.to_le_bytes());
|
|
for unit in [0x0007u16, b'A' as u16, 0x0000, 0x0007, b'B' as u16, 0x0000] {
|
|
buf.extend_from_slice(&unit.to_le_bytes());
|
|
}
|
|
let parsed = ComObjRef::parse(&buf).unwrap();
|
|
assert_eq!(parsed.dual_string_entries_decoded.len(), 2);
|
|
assert_eq!(parsed.dual_string_entries_decoded[0].value, "A");
|
|
assert!(!parsed.dual_string_entries_decoded[0].is_security_binding);
|
|
assert_eq!(parsed.dual_string_entries_decoded[1].value, "B");
|
|
assert!(parsed.dual_string_entries_decoded[1].is_security_binding);
|
|
}
|
|
|
|
#[test]
|
|
fn non_printable_codeunit_escaped_as_hex() {
|
|
// tower=0x0007, then a non-printable u16 (0x0100), then 'a', then 0x0000.
|
|
let mut buf = vec![0u8; 68];
|
|
buf[0..4].copy_from_slice(&0x574F_454Du32.to_le_bytes());
|
|
let entries: u16 = 4;
|
|
buf[DUAL_STRING_ENTRIES_OFFSET..DUAL_STRING_ENTRIES_OFFSET + 2]
|
|
.copy_from_slice(&entries.to_le_bytes());
|
|
buf[DUAL_STRING_SECURITY_OFFSET_OFFSET..DUAL_STRING_SECURITY_OFFSET_OFFSET + 2]
|
|
.copy_from_slice(&entries.to_le_bytes());
|
|
for unit in [0x0007u16, 0x0100, b'a' as u16, 0x0000] {
|
|
buf.extend_from_slice(&unit.to_le_bytes());
|
|
}
|
|
let parsed = ComObjRef::parse(&buf).unwrap();
|
|
assert_eq!(parsed.dual_string_entries_decoded.len(), 1);
|
|
// Expect "<0100>a" per the printable-ASCII escape rule
|
|
// (ComObjRef.cs:82-91).
|
|
assert_eq!(parsed.dual_string_entries_decoded[0].value, "<0100>a");
|
|
}
|
|
|
|
/// Captured OBJREF from `captures/057-managed-callback-route-service-trace-saved/probe.stdout.txt:6`
|
|
/// (`managed_callback_objref_hex`). 366 bytes; produced by the .NET
|
|
/// managed-callback exporter via `MarshalIUnknownObjRef` in the live
|
|
/// probe. Used to verify that the Rust parser interprets a real-world
|
|
/// OBJREF identically to the .NET reference.
|
|
const CAPTURED_OBJREF_HEX: &str = "4D454F5701000000F7929FB448C769418ECAA0670B012746800A000005000000750CC6C8BA9B1EFD3BF71E12FEE5B5C1022C0000DC32FFFF8AC67645E6ED23FF95007F0007004400450053004B0054004F0050002D0036004A004C0033004B004B004F0000000700310030002E003100300030002E0030002E0034003800000007003100370032002E00320039002E003200320034002E0031000000070066006400650031003A0061006500340031003A0038006100300030003A0034003500320061003A0062006200340031003A0065006500370065003A0035006600640034003A0064006300310038000000070066006400650031003A0061006500340031003A0038006100300030003A0034003500320061003A0035003000620031003A0038003400360066003A0037006200350031003A006500610034003000000000000900FFFF00001E00FFFF00001000FFFF00000A00FFFF00001600FFFF00001F00FFFF00000E00FFFF00000000";
|
|
|
|
fn hex_decode(hex: &str) -> Vec<u8> {
|
|
let bytes = hex.as_bytes();
|
|
assert!(bytes.len() % 2 == 0);
|
|
let mut out = Vec::with_capacity(bytes.len() / 2);
|
|
for chunk in bytes.chunks(2) {
|
|
let hi = (chunk[0] as char).to_digit(16).unwrap() as u8;
|
|
let lo = (chunk[1] as char).to_digit(16).unwrap() as u8;
|
|
out.push((hi << 4) | lo);
|
|
}
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn captured_objref_parses() {
|
|
let bytes = hex_decode(CAPTURED_OBJREF_HEX);
|
|
// probe.stdout.txt:5 reports managed_callback_objref_size=366.
|
|
assert_eq!(bytes.len(), 366);
|
|
|
|
let parsed = ComObjRef::parse(&bytes).unwrap();
|
|
|
|
// Signature is the canonical "MEOW".
|
|
assert_eq!(parsed.signature, OBJREF_SIGNATURE);
|
|
// OBJREF_STANDARD.
|
|
assert_eq!(parsed.flags, 1);
|
|
// public_refs = 5 (per the captured bytes 28..32 = 05 00 00 00).
|
|
assert_eq!(parsed.public_refs, 5);
|
|
// Captured bytes at offset 64..68 are `95 00 7F 00`:
|
|
// dual_string_entries (u16 LE) = 0x0095 = 149
|
|
// dual_string_security_offset (u16 LE) = 0x007F = 127
|
|
// 149 u16 units from offset 68 onwards exactly fills the remaining
|
|
// 366 - 68 = 298 bytes (149 * 2), so the entries count saturates the
|
|
// buffer — confirming the parser's `min(entries, data.len()/2)`
|
|
// bound at `ComObjRef.cs:59` produces the same effective walk length.
|
|
assert_eq!(parsed.dual_string_entries, 0x0095);
|
|
assert_eq!(parsed.dual_string_security_offset, 0x007F);
|
|
|
|
// First decoded string-binding is the hostname over ncacn_ip_tcp.
|
|
// The probe was run on host DESKTOP-6JL3KKO per the captured UTF-16
|
|
// bytes immediately following the dual-string header.
|
|
let first = &parsed.dual_string_entries_decoded[0];
|
|
assert_eq!(first.tower_id, 0x0007);
|
|
assert_eq!(first.protocol, "ncacn_ip_tcp");
|
|
assert_eq!(first.value, "DESKTOP-6JL3KKO");
|
|
assert!(!first.is_security_binding);
|
|
|
|
// Subsequent string-bindings are the IPv4 + IPv6 endpoint addresses.
|
|
// Confirm we got at least 4 string-bindings (host + v4 + 2x v6) plus
|
|
// the security-binding entries.
|
|
assert!(parsed.dual_string_entries_decoded.len() >= 5);
|
|
|
|
// At least one entry must be a security binding (entries past
|
|
// security_offset). The "0900FFFF" sequence in the captured bytes
|
|
// decodes as tower=0x0009 (ncacn_np) with a single non-printable
|
|
// u16 0xFFFF — appears in the security-binding tail.
|
|
assert!(
|
|
parsed
|
|
.dual_string_entries_decoded
|
|
.iter()
|
|
.any(|e| e.is_security_binding),
|
|
"expected at least one security binding in captured OBJREF"
|
|
);
|
|
|
|
// The diagnostic emitter must produce exactly 11 lines.
|
|
let lines = parsed.to_diagnostic_lines();
|
|
assert_eq!(lines.len(), 11);
|
|
assert_eq!(lines[0], "objref_signature=0x574F454D");
|
|
}
|
|
|
|
#[test]
|
|
fn guid_display_matches_dotnet_d_format() {
|
|
// .NET Guid("F7929FB4-48C7-6941-8ECA-A0670B012746".replace order):
|
|
// The 16-byte sequence F7 92 9F B4 48 C7 69 41 8E CA A0 67 0B 01 27 46
|
|
// displays as "b49f92f7-c748-4169-8eca-a0670b012746" — first three
|
|
// groups are byte-swapped (LE on wire, BE in display).
|
|
let g = Guid([
|
|
0xF7, 0x92, 0x9F, 0xB4, 0x48, 0xC7, 0x69, 0x41, 0x8E, 0xCA, 0xA0, 0x67, 0x0B, 0x01,
|
|
0x27, 0x46,
|
|
]);
|
|
assert_eq!(format!("{}", g), "b49f92f7-c748-4169-8eca-a0670b012746");
|
|
}
|
|
|
|
#[test]
|
|
fn header_length_constant() {
|
|
assert_eq!(ComObjRef::HEADER_LEN, 68);
|
|
assert_eq!(OBJREF_HEADER_LEN, 68);
|
|
assert_eq!(OBJREF_SIGNATURE, 0x574F_454D);
|
|
}
|
|
|
|
// ---- ComObjRefBuilder tests --------------------------------------
|
|
|
|
#[test]
|
|
fn builder_emits_meow_header_and_flags() {
|
|
let iid = Guid::new([0xAA; 16]);
|
|
let ipid = Guid::new([0xBB; 16]);
|
|
let buf = ComObjRefBuilder::create_standard_objref(
|
|
iid,
|
|
0x280,
|
|
5,
|
|
0x0123_4567_89AB_CDEF,
|
|
0xFEDC_BA98_7654_3210,
|
|
ipid,
|
|
&["host[5985]"],
|
|
);
|
|
assert!(buf.len() >= OBJREF_HEADER_LEN);
|
|
// Signature
|
|
assert_eq!(&buf[0..4], &OBJREF_SIGNATURE.to_le_bytes());
|
|
// flags = 1 (OBJREF_STANDARD)
|
|
assert_eq!(&buf[4..8], &1u32.to_le_bytes());
|
|
// IID
|
|
assert_eq!(&buf[8..24], iid.as_bytes());
|
|
// std_flags
|
|
assert_eq!(&buf[24..28], &0x280u32.to_le_bytes());
|
|
// public_refs
|
|
assert_eq!(&buf[28..32], &5u32.to_le_bytes());
|
|
// OXID/OID
|
|
assert_eq!(&buf[32..40], &0x0123_4567_89AB_CDEFu64.to_le_bytes());
|
|
assert_eq!(&buf[40..48], &0xFEDC_BA98_7654_3210u64.to_le_bytes());
|
|
// IPID
|
|
assert_eq!(&buf[48..64], ipid.as_bytes());
|
|
}
|
|
|
|
#[test]
|
|
fn builder_round_trips_through_parser() {
|
|
// The emitted OBJREF must parse back through ComObjRef::parse with
|
|
// the same key fields.
|
|
let iid = Guid::new([0x11; 16]);
|
|
let ipid = Guid::new([0x22; 16]);
|
|
let buf = ComObjRefBuilder::create_standard_objref(
|
|
iid,
|
|
0x280,
|
|
5,
|
|
0x1111_2222_3333_4444,
|
|
0x5555_6666_7777_8888,
|
|
ipid,
|
|
&["DESKTOP[12345]"],
|
|
);
|
|
|
|
let parsed = ComObjRef::parse(&buf).unwrap();
|
|
assert_eq!(parsed.signature, OBJREF_SIGNATURE);
|
|
assert_eq!(parsed.flags, 1);
|
|
assert_eq!(parsed.iid, iid);
|
|
assert_eq!(parsed.standard_flags, 0x280);
|
|
assert_eq!(parsed.public_refs, 5);
|
|
assert_eq!(parsed.oxid, 0x1111_2222_3333_4444);
|
|
assert_eq!(parsed.oid, 0x5555_6666_7777_8888);
|
|
assert_eq!(parsed.ipid, ipid);
|
|
|
|
// First decoded entry should be the ncacn_ip_tcp string binding,
|
|
// and at least one security binding (auth-service tail) follows.
|
|
let first = &parsed.dual_string_entries_decoded[0];
|
|
assert_eq!(first.tower_id, 0x0007);
|
|
assert_eq!(first.protocol, "ncacn_ip_tcp");
|
|
assert_eq!(first.value, "DESKTOP[12345]");
|
|
assert!(!first.is_security_binding);
|
|
|
|
let security_count = parsed
|
|
.dual_string_entries_decoded
|
|
.iter()
|
|
.filter(|e| e.is_security_binding)
|
|
.count();
|
|
assert!(
|
|
security_count >= 1,
|
|
"expected at least one security binding, got {security_count}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn builder_security_offset_matches_dotnet_formula() {
|
|
// security_offset = sum(1 + binding.len() + 1) + 1 (cs:348).
|
|
// For one binding "host[12]" (8 chars): 1 + 8 + 1 + 1 = 11.
|
|
let buf = ComObjRefBuilder::create_standard_objref(
|
|
Guid::ZERO,
|
|
0,
|
|
5,
|
|
0,
|
|
0,
|
|
Guid::ZERO,
|
|
&["host[12]"],
|
|
);
|
|
let security_offset = u16::from_le_bytes([buf[66], buf[67]]);
|
|
assert_eq!(security_offset, 11);
|
|
}
|
|
|
|
#[test]
|
|
fn builder_two_bindings_security_offset() {
|
|
// Two bindings: "a[1]" (4) + "b[2]" (4):
|
|
// (1+4+1) + (1+4+1) + 1 = 13.
|
|
let buf = ComObjRefBuilder::create_standard_objref(
|
|
Guid::ZERO,
|
|
0,
|
|
5,
|
|
0,
|
|
0,
|
|
Guid::ZERO,
|
|
&["a[1]", "b[2]"],
|
|
);
|
|
let security_offset = u16::from_le_bytes([buf[66], buf[67]]);
|
|
assert_eq!(security_offset, 13);
|
|
}
|
|
|
|
#[test]
|
|
fn builder_emits_seven_security_entries() {
|
|
// Each security entry contributes 3 u16 words [tower_id, 0xFFFF, 0].
|
|
// Total security words = 7 * 3 = 21, plus a trailing 0 = 22.
|
|
// String section for one binding "h[1]" (4 chars): 1+4+1+1 = 7 words.
|
|
// Total entries = 7 + 22 = 29.
|
|
let buf =
|
|
ComObjRefBuilder::create_standard_objref(Guid::ZERO, 0, 5, 0, 0, Guid::ZERO, &["h[1]"]);
|
|
let entries = u16::from_le_bytes([buf[64], buf[65]]);
|
|
assert_eq!(entries, 29);
|
|
}
|
|
|
|
#[test]
|
|
fn builder_auth_services_table_matches_dotnet_order() {
|
|
// The auth-service tower ids in the security tail must appear in the
|
|
// order the .NET reference writes them (cs:362).
|
|
assert_eq!(
|
|
CALLBACK_OBJREF_AUTH_SERVICES,
|
|
[0x0009, 0x001E, 0x0010, 0x000A, 0x0016, 0x001F, 0x000E]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn builder_total_buffer_length_matches_words_count() {
|
|
// entries * 2 + HEADER_LEN
|
|
let buf =
|
|
ComObjRefBuilder::create_standard_objref(Guid::ZERO, 0, 5, 0, 0, Guid::ZERO, &["x[1]"]);
|
|
let entries = u16::from_le_bytes([buf[64], buf[65]]) as usize;
|
|
assert_eq!(buf.len(), OBJREF_HEADER_LEN + entries * 2);
|
|
}
|
|
}
|