Files
mxaccess/rust/crates/mxaccess-codec/src/observed_write_template.rs
T
Joseph Doherty e79e289743 [F42] cargo doc --workspace --no-deps clean (0 warnings)
Fix all 33 rustdoc warnings across the workspace:

- Unresolved intra-doc links: rewrite [`name`] → either backtick text
  (when not actually a link) or fully-qualified `[Type::method]` /
  `[crate::module::name]` form. Affected: mxaccess-codec
  (asb_variant, item_control, metadata_query, observed_write_template,
  reference_handle, write_message), mxaccess-rpc (pdu), mxaccess-nmx
  (client), mxaccess-asb-nettcp (nmf), mxaccess-callback (exporter),
  mxaccess (asb_session, session, lib).
- Bracket-text being interpreted as link refs (e.g. `body[17]` →
  `` `body[17]` ``).
- Private-item references in public docs (CALLBACK_BROADCAST_CAPACITY,
  recover_connection_core, mxvalue_to_writevalue) reduced to
  backtick-text since they aren't part of the public API.

`RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` now
exits clean. Workspace 759 tests pass; clippy clean.

Defers `#![warn(missing_docs)]` lint to a future pass — the cleanup
target is the broken-link warnings, which are signal; missing-docs
would surface hundreds of low-priority public-item gaps that are out
of scope for this F-number.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:39:51 -04:00

905 lines
37 KiB
Rust

//! `ObservedWriteBodyTemplate` — observed-Write body round-trip preserver.
//!
//! Direct port of `src/MxNativeCodec/ObservedWriteBodyTemplate.cs`.
//!
//! ## What this is for
//!
//! The template path takes a *captured* Write body (real bytes from
//! `captures/0NN-frida-write-*`) and replays it with only the value slot
//! replaced. Every other byte — the prefix, the `cmd + version + handle
//! projection + wire_kind` header, the trailing suffix (clientToken,
//! writeIndex, the `-1 i16` discriminator, any padding) — is preserved
//! **verbatim**. This is one of the cornerstones of CLAUDE.md's "preserve
//! unknown bytes" rule (project root `CLAUDE.md`): some flows depend on
//! byte-for-byte parity with native MXAccess, and the captured suffix
//! contains bytes whose meaning is unproven.
//!
//! ## Layout assumptions (from `ObservedWriteBodyTemplate.cs:9-49`)
//!
//! Three offset constants from the .NET source:
//!
//! - `FixedValueOffset = 18` (`.cs:9`) — value slot for scalar types
//! (Boolean / Int32 / Float32 / Float64).
//! - `VariableValueOffset = 26` (`.cs:10`) — value slot for variable types
//! (String / DateTime), preceded by 8 bytes of length headers at offsets
//! 18..22 (outer_length) and 22..26 (inner_length).
//! - `ArrayValueOffset = 28` (`.cs:11`) — value slot for arrays, preceded by
//! 10 bytes (zeros at 18..22, count u16 at 22, element_width u16 at 24,
//! zeros at 26..28).
//!
//! The trailing suffix length is then implied by the captured body size:
//!
//! - **Fixed:** suffix starts at `FixedValueOffset + valueWidth` and runs
//! to `body.Length - sizeof(int)`. The trailing 4 bytes are the writeIndex.
//! `(.cs:96-109)`.
//! - **Variable:** suffix starts at `VariableValueOffset + valueByteLength`
//! where `valueByteLength = body.Slice(22, 4)` (read **unconditionally**).
//! Trailing writeIndex still 4 bytes. `(.cs:111-130)`.
//! - **Array:** suffix is exactly the **last 18 bytes**, of which the first
//! 14 are stored in `_suffixBeforeWriteIndex` and the last 4 are the
//! writeIndex. `(.cs:132-144)`.
//!
//! ## Round-trip preservation
//!
//! `Encode` (`.cs:51-64`) writes:
//!
//! 1. The captured prefix (`_prefix`, raw bytes) — preserved verbatim.
//! 2. The freshly-encoded value bytes from `encode_value_bytes`.
//! 3. The captured suffix (`_suffixBeforeWriteIndex`) — preserved verbatim.
//! 4. The fresh `writeIndex` as i32 LE in the trailing 4 bytes.
//!
//! Then it calls `PatchVariableLengths` and `PatchArrayDescriptor` to keep
//! the embedded length fields consistent with the new value bytes
//! (`.cs:378-411`).
//!
//! ## hasDetailStatus audit (Q7 follow-up)
//!
//! `ObservedWriteBodyTemplate.cs` does not take any `has_*` boolean
//! parameter. `CreateVariable` reads `body.Slice(22, 4)` unconditionally
//! (`.cs:118`); `CreateArray` reads `body.Slice(22, 2)` unconditionally
//! (via the count in DecodeBooleanArray etc., `.cs:198, 221, 251, 281, 337`);
//! `Decode*` functions read fixed-offset fields unconditionally per kind.
//! No conditional read patterns to mirror. **Audit: clean.**
//!
//! ## Public-API differences from .NET
//!
//! - The .NET `Decode` returns `object` and accepts the body to decode at
//! call time. The Rust port exposes [`ObservedWriteBodyTemplate::with_value`]
//! that returns a fresh body with the value replaced (the single most
//! common use case in probes / replay), plus `with_int32`, `with_boolean`,
//! etc. helpers.
//! - The .NET `Encode` takes a boxed `object` value; the Rust port takes a
//! typed [`crate::MxValue`] (no runtime conversion).
//! - The Rust port requires the captured kind to match the kind of the
//! replacement value — preventing accidentally replacing an Int32 with a
//! Float32 (which would corrupt the suffix offsets).
// Direct byte indexing — see reference_handle.rs for rationale.
#![allow(clippy::indexing_slicing)]
use crate::error::CodecError;
use crate::{MxValue, MxValueKind};
/// Value-slot offset for fixed-width scalars (`ObservedWriteBodyTemplate.cs:9`).
pub const FIXED_VALUE_OFFSET: usize = 18;
/// Value-slot offset for variable-length scalars
/// (`ObservedWriteBodyTemplate.cs:10`).
pub const VARIABLE_VALUE_OFFSET: usize = 26;
/// Value-slot offset for arrays (`ObservedWriteBodyTemplate.cs:11`).
pub const ARRAY_VALUE_OFFSET: usize = 28;
/// Round-trip preserver for a captured Write body (`0x37`) or
/// SecuredWrite2 body (`0x38`).
///
/// Stores three pieces:
///
/// - The captured *prefix* up to but not including the value slot.
/// - The captured *suffix* starting just after the value slot, up to but
/// not including the trailing 4-byte writeIndex.
/// - The captured kind (so we can later patch length fields correctly).
///
/// The prefix and suffix carry every byte unchanged; the value slot is
/// rewritten on each [`Self::with_value`] call, and the trailing writeIndex
/// is rewritten with whatever the caller passes to encode.
#[derive(Debug, Clone)]
pub struct ObservedWriteBodyTemplate {
kind: MxValueKind,
/// Captured opcode at body[0]. Either `0x37` (Write) or `0x38`
/// (SecuredWrite2) — both share the value-slot layout per the .cs
/// constants. Stored separately for [`Self::command`] without re-reading
/// `prefix`.
command: u8,
prefix: Vec<u8>,
suffix_before_write_index: Vec<u8>,
}
impl ObservedWriteBodyTemplate {
/// Capture a Write body and split it into prefix / suffix around the
/// value slot.
///
/// Mirrors `FromObserved` (`ObservedWriteBodyTemplate.cs:26-49`). The
/// caller declares the `kind` because the captured body alone does not
/// disambiguate `0x05` String vs DateTime or `0x45` StringArray vs
/// DateTimeArray (the encoder collapses them — see
/// `crate::write_message` module doc).
///
/// # Errors
///
/// - [`CodecError::ShortRead`] if `observed_body.len() < 24`
/// (`.cs:28-31`).
/// - [`CodecError::Decode`] if the kind is unsupported, or if the body
/// is too short for its declared kind, or if a variable-length body
/// has invalid embedded lengths.
pub fn from_observed(kind: MxValueKind, observed_body: &[u8]) -> Result<Self, CodecError> {
// `.cs:28-31` — minimum length 24 bytes regardless of kind.
if observed_body.len() < 24 {
return Err(CodecError::ShortRead {
expected: 24,
actual: observed_body.len(),
});
}
match kind {
// `.cs:35-38` — fixed-width scalars.
MxValueKind::Boolean | MxValueKind::Int32 | MxValueKind::Float32 => {
Self::create_fixed(kind, observed_body, 4)
}
MxValueKind::Float64 => Self::create_fixed(kind, observed_body, 8),
// `.cs:39-40` — variable-width scalars.
MxValueKind::String | MxValueKind::DateTime => {
Self::create_variable(kind, observed_body)
}
// `.cs:41-46` — arrays.
MxValueKind::BoolArray
| MxValueKind::Int32Array
| MxValueKind::Float32Array
| MxValueKind::Float64Array
| MxValueKind::StringArray
| MxValueKind::DateTimeArray => Self::create_array(kind, observed_body),
// `.cs:47` — anything else throws.
MxValueKind::ElapsedTime | MxValueKind::Unknown => Err(CodecError::Decode {
offset: 17,
reason: "observed-write template: unsupported value kind",
buffer_len: observed_body.len(),
}),
}
}
/// Captured opcode at `body[0]`. Mirrors `_prefix[0]`.
pub fn command(&self) -> u8 {
self.command
}
/// Captured wire-kind byte at `body[17]`. Drawn from the captured prefix,
/// not from the runtime [`MxValueKind`] (which can disambiguate
/// String vs DateTime past the encoder collapse).
pub fn wire_kind(&self) -> u8 {
// body[17] sits inside the prefix for all three families.
self.prefix[17]
}
/// The kind this template was captured against.
pub fn kind(&self) -> MxValueKind {
self.kind
}
/// Borrow the captured prefix bytes (everything before the value slot).
pub fn prefix(&self) -> &[u8] {
&self.prefix
}
/// Borrow the captured suffix bytes (between the value slot and the
/// trailing 4-byte writeIndex).
pub fn suffix_before_write_index(&self) -> &[u8] {
&self.suffix_before_write_index
}
/// Emit a Write body with the value slot replaced.
///
/// Mirrors `Encode` (`ObservedWriteBodyTemplate.cs:51-64`):
/// 1. Allocate `prefix.len() + value_bytes.len() + suffix.len() + 4`.
/// 2. Copy prefix verbatim.
/// 3. Copy fresh value bytes.
/// 4. Copy suffix verbatim.
/// 5. Write i32 LE writeIndex into the trailing 4 bytes.
/// 6. Patch embedded length fields (variable / array).
///
/// # Errors
///
/// - [`CodecError::Decode`] if `value.kind()` doesn't match the
/// captured kind. The .NET reference does not check this — it boxes
/// any `object` and lets the per-kind encoder throw — but mismatched
/// replacement would silently corrupt the suffix offsets, so the Rust
/// port enforces it.
pub fn with_value(&self, value: &MxValue, write_index: i32) -> Result<Vec<u8>, CodecError> {
// Strict kind check (Rust-port tightening; see module doc).
// The encoder collapses StringArray and DateTimeArray onto the same
// wire kind, so accept that pair as compatible.
let val_kind = value.kind();
let kinds_match = val_kind == self.kind
|| (val_kind == MxValueKind::StringArray && self.kind == MxValueKind::DateTimeArray)
|| (val_kind == MxValueKind::DateTimeArray && self.kind == MxValueKind::StringArray)
|| (val_kind == MxValueKind::String && self.kind == MxValueKind::DateTime)
|| (val_kind == MxValueKind::DateTime && self.kind == MxValueKind::String);
if !kinds_match {
return Err(CodecError::Decode {
offset: 17,
reason: "observed-write template: replacement value kind does not match captured kind",
buffer_len: 0,
});
}
let value_bytes = encode_value_bytes(value, self.kind)?;
let body_len =
self.prefix.len() + value_bytes.len() + self.suffix_before_write_index.len() + 4;
let mut body = vec![0u8; body_len];
body[..self.prefix.len()].copy_from_slice(&self.prefix);
let value_start = self.prefix.len();
body[value_start..value_start + value_bytes.len()].copy_from_slice(&value_bytes);
let suffix_start = value_start + value_bytes.len();
body[suffix_start..suffix_start + self.suffix_before_write_index.len()]
.copy_from_slice(&self.suffix_before_write_index);
write_i32_le(&mut body, body_len - 4, write_index);
self.patch_variable_lengths(&mut body, value_bytes.len());
self.patch_array_descriptor(&mut body, value_bytes.len());
Ok(body)
}
/// Convenience: replace the value with an i32. Errors if the captured
/// kind isn't [`MxValueKind::Int32`].
pub fn with_int32(&self, value: i32, write_index: i32) -> Result<Vec<u8>, CodecError> {
self.with_value(&MxValue::Int32(value), write_index)
}
/// Convenience: replace the value with a bool. Errors if the captured
/// kind isn't [`MxValueKind::Boolean`].
pub fn with_boolean(&self, value: bool, write_index: i32) -> Result<Vec<u8>, CodecError> {
self.with_value(&MxValue::Boolean(value), write_index)
}
/// Convenience: replace the value with an f32.
pub fn with_float32(&self, value: f32, write_index: i32) -> Result<Vec<u8>, CodecError> {
self.with_value(&MxValue::Float32(value), write_index)
}
/// Convenience: replace the value with an f64.
pub fn with_float64(&self, value: f64, write_index: i32) -> Result<Vec<u8>, CodecError> {
self.with_value(&MxValue::Float64(value), write_index)
}
/// Convenience: replace the value with a UTF-16LE string.
pub fn with_string(&self, value: &str, write_index: i32) -> Result<Vec<u8>, CodecError> {
self.with_value(&MxValue::String(value.to_string()), write_index)
}
/// Decode the trailing writeIndex from a body that was emitted by this
/// template. Mirrors `DecodeWriteIndex` (`.cs:86-94`).
///
/// # Errors
///
/// - [`CodecError::ShortRead`] if `body` has fewer than 4 bytes.
pub fn decode_write_index(body: &[u8]) -> Result<i32, CodecError> {
if body.len() < 4 {
return Err(CodecError::ShortRead {
expected: 4,
actual: body.len(),
});
}
Ok(read_i32_le(body, body.len() - 4))
}
// ---- Private constructors --------------------------------------------
/// `CreateFixed` (`ObservedWriteBodyTemplate.cs:96-109`).
fn create_fixed(
kind: MxValueKind,
body: &[u8],
value_width: usize,
) -> Result<Self, CodecError> {
let suffix_start = FIXED_VALUE_OFFSET + value_width;
// `.cs:99-103` — suffix length must be non-negative.
if body.len() < suffix_start + 4 {
return Err(CodecError::ShortRead {
expected: suffix_start + 4,
actual: body.len(),
});
}
let suffix_length = body.len() - suffix_start - 4;
Ok(Self {
kind,
command: body[0],
prefix: body[..FIXED_VALUE_OFFSET].to_vec(),
suffix_before_write_index: body[suffix_start..suffix_start + suffix_length].to_vec(),
})
}
/// `CreateVariable` (`ObservedWriteBodyTemplate.cs:111-130`). Reads the
/// inner length at offset 22 **unconditionally** — there is no
/// "has X" boolean here, mirroring the .NET source.
fn create_variable(kind: MxValueKind, body: &[u8]) -> Result<Self, CodecError> {
// `.cs:113-116` — minimum length check.
if body.len() < VARIABLE_VALUE_OFFSET + 4 {
return Err(CodecError::ShortRead {
expected: VARIABLE_VALUE_OFFSET + 4,
actual: body.len(),
});
}
// `.cs:118` — value byte length read at offset 22 unconditionally.
let value_byte_length = read_i32_le(body, 22);
if value_byte_length < 2 {
return Err(CodecError::Decode {
offset: 22,
reason: "observed-write template: variable value_byte_length < 2",
buffer_len: body.len(),
});
}
let value_byte_length = value_byte_length as usize;
let suffix_start = VARIABLE_VALUE_OFFSET + value_byte_length;
if body.len() < suffix_start + 4 {
return Err(CodecError::Decode {
offset: 22,
reason: "observed-write template: variable body too short for declared lengths",
buffer_len: body.len(),
});
}
let suffix_length = body.len() - suffix_start - 4;
Ok(Self {
kind,
command: body[0],
prefix: body[..VARIABLE_VALUE_OFFSET].to_vec(),
suffix_before_write_index: body[suffix_start..suffix_start + suffix_length].to_vec(),
})
}
/// `CreateArray` (`ObservedWriteBodyTemplate.cs:132-144`). The .NET
/// reference takes the **last 18 bytes** as the suffix
/// (`suffixStart = body.Length - 18`), of which the first 14 are stored
/// in `_suffixBeforeWriteIndex` (`.cs:143`). This loses the array
/// payload bytes between offset 28 and `body.Length - 18` — they are
/// regenerated by [`encode_value_bytes`] from the supplied value.
fn create_array(kind: MxValueKind, body: &[u8]) -> Result<Self, CodecError> {
// `.cs:134-137` — body must hold at least 18 trailing bytes plus
// the 28-byte prefix.
if body.len() < ARRAY_VALUE_OFFSET + 18 {
return Err(CodecError::ShortRead {
expected: ARRAY_VALUE_OFFSET + 18,
actual: body.len(),
});
}
let suffix_start = body.len() - 18;
Ok(Self {
kind,
command: body[0],
// `.cs:142` — prefix is the leading 28 bytes.
prefix: body[..ARRAY_VALUE_OFFSET].to_vec(),
// `.cs:143` — suffix_before_write_index is exactly 14 bytes
// (the trailing 18 minus the 4-byte writeIndex slot).
suffix_before_write_index: body[suffix_start..suffix_start + 14].to_vec(),
})
}
// ---- Length / descriptor patches -------------------------------------
/// `PatchVariableLengths` (`ObservedWriteBodyTemplate.cs:378-388`).
/// Updates the embedded outer/inner length fields at offsets 18 and 22
/// for variable-width kinds. No-op for everything else.
fn patch_variable_lengths(&self, body: &mut [u8], value_byte_length: usize) {
if !matches!(self.kind, MxValueKind::String | MxValueKind::DateTime) {
return;
}
// `.cs:385` — body[18..22] = value_byte_length + 4.
write_i32_le(body, 18, value_byte_length as i32 + 4);
// `.cs:386` — body[22..26] = value_byte_length.
write_i32_le(body, 22, value_byte_length as i32);
}
/// `PatchArrayDescriptor` (`ObservedWriteBodyTemplate.cs:390-411`).
/// Updates the array element count u16 at offset 22 for fixed-element
/// arrays. No-op for variable-element arrays (StringArray /
/// DateTimeArray) and for non-array kinds — matching the early returns
/// at `.cs:392-400`.
fn patch_array_descriptor(&self, body: &mut [u8], value_byte_length: usize) {
let element_size = match self.kind {
MxValueKind::BoolArray => 2,
MxValueKind::Int32Array | MxValueKind::Float32Array => 4,
MxValueKind::Float64Array => 8,
// `.cs:397-400` — variable arrays return early; descriptor not patched.
_ => return,
};
let count = value_byte_length / element_size;
// `.cs:410` — body[22..24] = checked u16 count.
let count_u16: u16 = u16::try_from(count).unwrap_or(u16::MAX);
write_u16_le(body, 22, count_u16);
}
}
// ---- Value-byte encoders --------------------------------------------------
/// Encode the value bytes that go into the value slot. Mirrors `EncodeValue`
/// (`ObservedWriteBodyTemplate.cs:146-164`).
///
/// `kind` is the captured kind (used to disambiguate the encoder collapse
/// for String/DateTime and StringArray/DateTimeArray — though in this
/// template path the actual byte layout is identical for both halves of
/// each pair, so the distinction is presentational).
fn encode_value_bytes(value: &MxValue, _kind: MxValueKind) -> Result<Vec<u8>, CodecError> {
Ok(match value {
// `.cs:150` / `EncodeBoolean` (`.cs:166-171`). Literal byte
// patterns: true -> [0xff,0xff,0xff,0x00], false -> [0x00,0xff,0xff,0x00].
MxValue::Boolean(b) => encode_boolean_bytes(*b).to_vec(),
// `.cs:151` — i32 LE.
MxValue::Int32(v) => v.to_le_bytes().to_vec(),
// `.cs:152` — f32 via SingleToInt32Bits + i32 LE
// (`.cs:231-236` `EncodeFloat32`).
MxValue::Float32(v) => f32::to_bits(*v).to_le_bytes().to_vec(),
// `.cs:153` — f64 via DoubleToInt64Bits + i64 LE
// (`.cs:261-266` `EncodeFloat64`).
MxValue::Float64(v) => f64::to_bits(*v).to_le_bytes().to_vec(),
// `.cs:154` — UTF-16LE with 2-byte NUL trailer
// (`.cs:291-297` `EncodeUtf16String`).
MxValue::String(s) => encode_utf16_with_nul(s),
// `.cs:155` — DateTime is formatted with
// `"M/d/yyyy h:mm:ss tt"` and encoded as UTF-16LE+NUL. The Rust
// port carries the i64 FILETIME ticks; converting that to the
// .NET formatted string requires a calendar dependency outside
// the codec layer. The carrier expects a pre-formatted string.
MxValue::DateTime(_ticks) => {
return Err(CodecError::Decode {
offset: 0,
reason: "observed-write template: DateTime replacement requires a pre-formatted string; pass MxValue::String instead",
buffer_len: 0,
});
}
// `.cs:156` `EncodeBooleanArray` (`.cs:185-194`) — each element
// is i16 LE (-1 or 0).
MxValue::BoolArray(arr) => {
let mut bytes = Vec::with_capacity(arr.len() * 2);
for v in arr {
let i: i16 = if *v { -1 } else { 0 };
bytes.extend_from_slice(&i.to_le_bytes());
}
bytes
}
// `.cs:157` `EncodeInt32Array` (`.cs:208-217`).
MxValue::Int32Array(arr) => {
let mut bytes = Vec::with_capacity(arr.len() * 4);
for v in arr {
bytes.extend_from_slice(&v.to_le_bytes());
}
bytes
}
// `.cs:158` `EncodeFloat32Array` (`.cs:238-247`).
MxValue::Float32Array(arr) => {
let mut bytes = Vec::with_capacity(arr.len() * 4);
for v in arr {
bytes.extend_from_slice(&f32::to_bits(*v).to_le_bytes());
}
bytes
}
// `.cs:159` `EncodeFloat64Array` (`.cs:268-277`).
MxValue::Float64Array(arr) => {
let mut bytes = Vec::with_capacity(arr.len() * 8);
for v in arr {
bytes.extend_from_slice(&f64::to_bits(*v).to_le_bytes());
}
bytes
}
// `.cs:160` `EncodeVariableArray` (`.cs:317-333`).
MxValue::StringArray(arr) => encode_variable_array(arr.iter().map(String::as_str)),
// `.cs:161` — DateTimeArray formats each element. Same
// limitation as scalar DateTime; require pre-formatted strings.
MxValue::DateTimeArray(_arr) => {
return Err(CodecError::Decode {
offset: 0,
reason: "observed-write template: DateTimeArray replacement requires pre-formatted strings; pass MxValue::StringArray instead",
buffer_len: 0,
});
}
// `.cs:162` — InvalidOperationException for unsupported.
MxValue::ElapsedTime(_) => {
return Err(CodecError::Decode {
offset: 0,
reason: "observed-write template: ElapsedTime is not supported on the write side",
buffer_len: 0,
});
}
})
}
/// Boolean payload bytes — LITERALLY the same 4-byte pattern used by the
/// normal-write encoder (`.cs:166-171`,
/// matches `crate::write_message` `encode_boolean_value`).
const fn encode_boolean_bytes(value: bool) -> [u8; 4] {
if value {
[0xff, 0xff, 0xff, 0x00]
} else {
[0x00, 0xff, 0xff, 0x00]
}
}
/// UTF-16LE encoding with a trailing 2-byte NUL terminator. Mirrors
/// `EncodeUtf16String` (`ObservedWriteBodyTemplate.cs:291-297`).
fn encode_utf16_with_nul(value: &str) -> Vec<u8> {
let utf16: Vec<u16> = value.encode_utf16().collect();
let mut bytes = Vec::with_capacity(utf16.len() * 2 + 2);
for unit in &utf16 {
bytes.extend_from_slice(&unit.to_le_bytes());
}
bytes.push(0x00);
bytes.push(0x00);
bytes
}
/// Variable-array payload. Mirrors `EncodeVariableArray`
/// (`ObservedWriteBodyTemplate.cs:317-333`).
fn encode_variable_array<'a, I>(values: I) -> Vec<u8>
where
I: IntoIterator<Item = &'a str>,
{
let mut bytes = Vec::new();
for value in values {
let text_bytes = encode_utf16_with_nul(value);
let mut header = [0u8; 13];
// header[0..4] = 1 + 4 + 4 + textBytes.Length (`.cs:324`).
write_i32_le(&mut header, 0, 1i32 + 4 + 4 + text_bytes.len() as i32);
// header[4] = 0x05 (`.cs:325`).
header[4] = 0x05;
// header[5..9] = textBytes.Length + 4 (`.cs:326`).
write_i32_le(&mut header, 5, text_bytes.len() as i32 + 4);
// header[9..13] = textBytes.Length (`.cs:327`).
write_i32_le(&mut header, 9, text_bytes.len() as i32);
bytes.extend_from_slice(&header);
bytes.extend_from_slice(&text_bytes);
}
bytes
}
// ---- LE primitive helpers -------------------------------------------------
#[inline]
fn write_u16_le(bytes: &mut [u8], offset: usize, value: u16) {
bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
}
#[inline]
fn write_i32_le(bytes: &mut [u8], offset: usize, value: i32) {
bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
#[inline]
fn read_i32_le(bytes: &[u8], offset: usize) -> i32 {
i32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
])
}
// ===========================================================================
// Tests
// ===========================================================================
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod tests {
use super::*;
use crate::MxReferenceHandle;
use crate::write_message::{self, WriteValue};
fn sample_handle() -> MxReferenceHandle {
MxReferenceHandle::from_names(
1,
42,
17,
300,
"TestChildObject",
-1,
7,
0,
"TestInt",
false,
)
.unwrap()
}
fn observed_int32_body(value: i32, write_index: i32, client_token: u32) -> Vec<u8> {
write_message::encode(
&sample_handle(),
&WriteValue::Int32(value),
write_index,
client_token,
)
.unwrap()
}
fn observed_boolean_body(value: bool, write_index: i32, client_token: u32) -> Vec<u8> {
write_message::encode(
&sample_handle(),
&WriteValue::Boolean(value),
write_index,
client_token,
)
.unwrap()
}
fn observed_string_body(value: &str, write_index: i32, client_token: u32) -> Vec<u8> {
write_message::encode(
&sample_handle(),
&WriteValue::String(value.to_string()),
write_index,
client_token,
)
.unwrap()
}
fn observed_int32_array_body(values: &[i32], write_index: i32, client_token: u32) -> Vec<u8> {
write_message::encode(
&sample_handle(),
&WriteValue::Int32Array(values.to_vec()),
write_index,
client_token,
)
.unwrap()
}
// ---- Constants -------------------------------------------------------
#[test]
fn offset_constants_match_dotnet() {
// `ObservedWriteBodyTemplate.cs:9-11`.
assert_eq!(FIXED_VALUE_OFFSET, 18);
assert_eq!(VARIABLE_VALUE_OFFSET, 26);
assert_eq!(ARRAY_VALUE_OFFSET, 28);
}
// ---- Round-trip identity --------------------------------------------
#[test]
fn fixed_round_trip_preserves_bytes_when_value_unchanged() {
// Capture an Int32(123) body, replay with the same value — every
// byte should match.
let original = observed_int32_body(123, 7, 0xCAFE_BABE);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32, &original).unwrap();
let replayed = template.with_int32(123, 7).unwrap();
assert_eq!(replayed, original);
}
#[test]
fn variable_round_trip_preserves_bytes_when_value_unchanged() {
let original = observed_string_body("hello", 3, 0x12345678);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::String, &original).unwrap();
let replayed = template.with_string("hello", 3).unwrap();
assert_eq!(replayed, original);
}
#[test]
fn array_round_trip_preserves_suffix_when_value_unchanged() {
let original = observed_int32_array_body(&[1, 2, 3], 1, 0xABCD);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32Array, &original).unwrap();
let replayed = template
.with_value(&MxValue::Int32Array(vec![1, 2, 3]), 1)
.unwrap();
// The 14-byte suffix (last 18 minus writeIndex) must match.
let original_suffix_start = original.len() - 18;
let replayed_suffix_start = replayed.len() - 18;
assert_eq!(
&original[original_suffix_start..original_suffix_start + 14],
&replayed[replayed_suffix_start..replayed_suffix_start + 14]
);
// Trailing writeIndex field too.
assert_eq!(
&original[original.len() - 4..],
&replayed[replayed.len() - 4..]
);
}
// ---- Selective replacement -------------------------------------------
#[test]
fn replace_int32_only_changes_value_slot() {
let original = observed_int32_body(123, 7, 0xCAFE_BABE);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32, &original).unwrap();
let replaced = template.with_int32(456, 7).unwrap();
assert_eq!(replaced.len(), original.len());
// Everything except body[18..22] should match.
for (i, (&a, &b)) in original.iter().zip(replaced.iter()).enumerate() {
if (FIXED_VALUE_OFFSET..FIXED_VALUE_OFFSET + 4).contains(&i) {
continue;
}
assert_eq!(a, b, "byte at offset {i} should be preserved");
}
// The value slot reflects 456.
assert_eq!(
&replaced[FIXED_VALUE_OFFSET..FIXED_VALUE_OFFSET + 4],
&456i32.to_le_bytes()
);
}
#[test]
fn replace_boolean_uses_literal_4byte_pattern() {
// Boolean payload uses the literal 4-byte pattern, not a single byte.
let original = observed_boolean_body(true, 1, 0);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Boolean, &original).unwrap();
let replaced = template.with_boolean(false, 1).unwrap();
assert_eq!(
&replaced[FIXED_VALUE_OFFSET..FIXED_VALUE_OFFSET + 4],
&[0x00, 0xff, 0xff, 0x00]
);
// Re-replacing with true gives the original.
let back = template.with_boolean(true, 1).unwrap();
assert_eq!(
&back[FIXED_VALUE_OFFSET..FIXED_VALUE_OFFSET + 4],
&[0xff, 0xff, 0xff, 0x00]
);
assert_eq!(back, original);
}
#[test]
fn replace_string_with_different_length_grows_body_and_patches_lengths() {
// The .cs path supports same-length AND different-length string
// replacements: PatchVariableLengths rewrites both length fields
// (`.cs:378-388`).
let original = observed_string_body("hi", 1, 0);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::String, &original).unwrap();
let replaced = template.with_string("hello world", 1).unwrap();
// New length must reflect the new payload (UTF-16 + NUL = 24 bytes).
let utf16_len = "hello world".encode_utf16().count() * 2 + 2;
let outer = read_i32_le(&replaced, 18);
let inner = read_i32_le(&replaced, 22);
assert_eq!(inner, utf16_len as i32);
assert_eq!(outer, utf16_len as i32 + 4);
// The captured suffix (everything after the value slot) must
// still appear verbatim.
let suffix_start = VARIABLE_VALUE_OFFSET + utf16_len;
let suffix_len = template.suffix_before_write_index().len();
assert_eq!(
&replaced[suffix_start..suffix_start + suffix_len],
template.suffix_before_write_index()
);
}
// ---- Suffix preservation ---------------------------------------------
#[test]
fn suffix_clienttoken_and_writeindex_preserved_across_with_value_calls() {
let original = observed_int32_body(123, 0xAA, 0xDEADBEEF);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32, &original).unwrap();
// Replace value but keep writeIndex; the captured clientToken
// (in the suffix) must round-trip.
let replaced = template.with_int32(999, 0xAA).unwrap();
// For Int32 normal write, the suffix layout is:
// body[22..24] = -1 i16
// body[24..32] = 8-byte filler
// body[32..36] = clientToken u32
// body[36..40] = writeIndex i32
let client_token =
u32::from_le_bytes([replaced[32], replaced[33], replaced[34], replaced[35]]);
assert_eq!(client_token, 0xDEADBEEF);
let write_index =
i32::from_le_bytes([replaced[36], replaced[37], replaced[38], replaced[39]]);
assert_eq!(write_index, 0xAA);
}
#[test]
fn suffix_minus_one_marker_preserved() {
// The leading i16 of the normal suffix is -1 (0xFFFF); it lives in
// the captured suffix bytes, so any with_value call should preserve it.
let original = observed_int32_body(0, 1, 0);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32, &original).unwrap();
let replaced = template.with_int32(0xFEED, 99).unwrap();
let leading = i16::from_le_bytes([replaced[22], replaced[23]]);
assert_eq!(leading, -1);
}
#[test]
fn write_index_decoder() {
let original = observed_int32_body(0, 0xABCDEF, 0);
let decoded = ObservedWriteBodyTemplate::decode_write_index(&original).unwrap();
assert_eq!(decoded, 0xABCDEF);
}
// ---- Array kind: count patch and suffix -----------------------------
#[test]
fn array_descriptor_count_patched_on_replacement() {
// Original: 3 Int32 elements -> body[22..24] count = 3.
let original = observed_int32_array_body(&[10, 20, 30], 1, 0);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32Array, &original).unwrap();
// Replace with a 5-element array; descriptor count u16 must be 5.
let replaced = template
.with_value(&MxValue::Int32Array(vec![1, 2, 3, 4, 5]), 1)
.unwrap();
let count = u16::from_le_bytes([replaced[22], replaced[23]]);
assert_eq!(count, 5);
}
#[test]
fn array_string_descriptor_not_patched_on_replacement() {
// StringArray / DateTimeArray skip PatchArrayDescriptor (.cs:397-400).
let original = write_message::encode(
&sample_handle(),
&WriteValue::StringArray(vec!["a".into(), "b".into()]),
1,
0,
)
.unwrap();
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::StringArray, &original).unwrap();
let count_before = u16::from_le_bytes([original[22], original[23]]);
let replaced = template
.with_value(
&MxValue::StringArray(vec!["xx".into(), "yy".into(), "zz".into()]),
1,
)
.unwrap();
let count_after = u16::from_le_bytes([replaced[22], replaced[23]]);
// Per .NET behaviour, the count u16 is NOT patched for variable arrays.
assert_eq!(count_after, count_before);
}
// ---- Error paths -----------------------------------------------------
#[test]
fn from_observed_rejects_short_buffer() {
let err =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32, &[0u8; 23]).unwrap_err();
assert!(matches!(err, CodecError::ShortRead { .. }));
}
#[test]
fn from_observed_rejects_unsupported_kind() {
let original = observed_int32_body(0, 1, 0);
let err = ObservedWriteBodyTemplate::from_observed(MxValueKind::ElapsedTime, &original)
.unwrap_err();
assert!(matches!(err, CodecError::Decode { .. }));
}
#[test]
fn from_observed_rejects_short_array_body() {
// 28 + 18 - 1 = 45 bytes (one byte short).
let err = ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32Array, &[0u8; 45])
.unwrap_err();
assert!(matches!(err, CodecError::ShortRead { .. }));
}
#[test]
fn from_observed_rejects_invalid_variable_lengths() {
// Build a 30-byte body where body[22..26] declares a value length of 0.
let mut buf = vec![0u8; 30];
write_i32_le(&mut buf, 22, 0);
let err = ObservedWriteBodyTemplate::from_observed(MxValueKind::String, &buf).unwrap_err();
assert!(matches!(err, CodecError::Decode { .. }));
}
#[test]
fn with_value_rejects_kind_mismatch() {
let original = observed_int32_body(0, 1, 0);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32, &original).unwrap();
let err = template.with_value(&MxValue::Float32(1.0), 1).unwrap_err();
assert!(matches!(err, CodecError::Decode { .. }));
}
#[test]
fn command_and_wire_kind_accessors() {
let original = observed_int32_body(123, 1, 0);
let template =
ObservedWriteBodyTemplate::from_observed(MxValueKind::Int32, &original).unwrap();
// Normal Write opcode is 0x37; Int32 wire kind is 0x02.
assert_eq!(template.command(), 0x37);
assert_eq!(template.wire_kind(), 0x02);
}
}