e79e289743
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>
551 lines
20 KiB
Rust
551 lines
20 KiB
Rust
//! `NmxItemControlMessage` — NMX item-control body (advise / unadvise).
|
||
//!
|
||
//! Direct port of `src/MxNativeCodec/NmxItemControlMessage.cs`. The body
|
||
//! carries an advise-supervisory or unadvise command together with a 16-byte
|
||
//! item correlation GUID and a 14-byte projection of an [`crate::reference_handle::MxReferenceHandle`]
|
||
//! (handle bytes 6..20 — `object_id` through `attribute_index`).
|
||
//!
|
||
//! ## Wire layout
|
||
//!
|
||
//! Per `NmxItemControlMessage.cs:24-36, 63-81, 121-142`:
|
||
//!
|
||
//! ```text
|
||
//! offset size field notes
|
||
//! 0 1 command (u8) 0x1f AdviseSupervisory, 0x21 UnAdvise
|
||
//! 1 2 version (u16 LE) must be 1
|
||
//! 3 16 item_correlation_id (GUID) .NET layout (mixed-endian)
|
||
//! 19 2 advise extra (u16 LE) ONLY when command == AdviseSupervisory
|
||
//! [+2 if advise]
|
||
//! 19/21 2 object_id (u16 LE)
|
||
//! +2 2 object_signature (u16 LE)
|
||
//! +4 2 primitive_id (i16 LE)
|
||
//! +6 2 attribute_id (i16 LE)
|
||
//! +8 2 property_id (i16 LE)
|
||
//! +10 2 attribute_signature (u16 LE)
|
||
//! +12 2 attribute_index (i16 LE)
|
||
//! +14 4 tail (u32 LE) default 3 per cs:88
|
||
//! ```
|
||
//!
|
||
//! Total: 39 bytes for AdviseSupervisory, 37 bytes for UnAdvise.
|
||
//!
|
||
//! ## Opcode invariant
|
||
//!
|
||
//! `Advise` and `AdviseSupervisory` share opcode `0x1f` in the .NET enum
|
||
//! (`NmxItemControlMessage.cs:7-8`). The parser explicitly rejects anything
|
||
//! that is not `AdviseSupervisory` or `UnAdvise` (`cs:46-49`); there is no
|
||
//! 37-byte plain-Advise wire shape. The Rust port mirrors this: the public
|
||
//! command type only exposes `AdviseSupervisory` and `UnAdvise`.
|
||
|
||
// Direct byte indexing — see reference_handle.rs for rationale. Every read or
|
||
// write is preceded by an explicit length check that mirrors the .NET source's
|
||
// `ReadOnlySpan` slicing, so the resulting code reads as a 1:1 mirror of
|
||
// `BinaryPrimitives` calls.
|
||
#![allow(clippy::indexing_slicing)]
|
||
|
||
use crate::error::CodecError;
|
||
|
||
/// NMX item-control command opcode.
|
||
///
|
||
/// In the .NET reference this is a `byte` enum where `Advise` and
|
||
/// `AdviseSupervisory` are aliases for `0x1f` (`NmxItemControlMessage.cs:5-10`).
|
||
/// Only `AdviseSupervisory` and `UnAdvise` are accepted on the wire
|
||
/// (`cs:46-49`), so the Rust enum collapses the alias and exposes just those
|
||
/// two variants.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
#[repr(u8)]
|
||
pub enum NmxItemControlCommand {
|
||
/// `0x1f`. Per `NmxItemControlMessage.cs:8`.
|
||
AdviseSupervisory = 0x1f,
|
||
/// `0x21`. Per `NmxItemControlMessage.cs:9`.
|
||
UnAdvise = 0x21,
|
||
}
|
||
|
||
impl NmxItemControlCommand {
|
||
/// Map a wire byte to a command, mirroring the parser check at
|
||
/// `NmxItemControlMessage.cs:45-49`.
|
||
fn from_u8(value: u8) -> Result<Self, CodecError> {
|
||
match value {
|
||
0x1f => Ok(Self::AdviseSupervisory),
|
||
0x21 => Ok(Self::UnAdvise),
|
||
other => Err(CodecError::UnexpectedOpcode(other)),
|
||
}
|
||
}
|
||
|
||
fn to_u8(self) -> u8 {
|
||
self as u8
|
||
}
|
||
}
|
||
|
||
/// Wire-format constants from `NmxItemControlMessage.cs:24-28`.
|
||
const VERSION: u16 = 1;
|
||
const HEADER_LENGTH: usize = 3; // cmd(1) + version u16(2)
|
||
const GUID_LENGTH: usize = 16; // cs:26
|
||
const ADVISE_EXTRA_LENGTH: usize = 2; // cs:27
|
||
const PAYLOAD_LENGTH: usize = 18; // cs:28 — 7×u16 + u32 tail = 18 bytes
|
||
|
||
/// Default tail value used by `FromReferenceHandle` (`NmxItemControlMessage.cs:88`).
|
||
pub const DEFAULT_TAIL: u32 = 3;
|
||
|
||
/// Decoded NMX item-control body. The fields after `item_correlation_id`
|
||
/// project bytes 6..20 of an [`crate::reference_handle::MxReferenceHandle`] — see
|
||
/// `NmxItemControlMessage.cs:71-81, 134-141`.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub struct NmxItemControlMessage {
|
||
pub command: NmxItemControlCommand,
|
||
/// 16-byte GUID. Stored as raw bytes in .NET-`Guid` layout (the layout
|
||
/// produced by `Guid.TryWriteBytes` and consumed by `new Guid(span)` —
|
||
/// mixed-endian: little-endian `Data1`/`Data2`/`Data3`, big-endian
|
||
/// `Data4`/`Data4Tail`). The Rust port stays at the byte level so the
|
||
/// .NET-shape round-trips exactly. See `NmxItemControlMessage.cs:64, 127`.
|
||
pub item_correlation_id: [u8; GUID_LENGTH],
|
||
pub object_id: u16,
|
||
pub object_signature: u16,
|
||
pub primitive_id: i16,
|
||
pub attribute_id: i16,
|
||
pub property_id: i16,
|
||
pub attribute_signature: u16,
|
||
pub attribute_index: i16,
|
||
/// Trailing u32. Default `3` per `NmxItemControlMessage.cs:88`.
|
||
pub tail: u32,
|
||
}
|
||
|
||
impl NmxItemControlMessage {
|
||
/// Encoded length for a given command. Matches
|
||
/// `NmxItemControlMessage.GetEncodedLength` (`cs:30-36`):
|
||
/// 39 bytes for AdviseSupervisory, 37 bytes for UnAdvise.
|
||
#[must_use]
|
||
pub fn encoded_length(command: NmxItemControlCommand) -> usize {
|
||
HEADER_LENGTH
|
||
+ GUID_LENGTH
|
||
+ match command {
|
||
NmxItemControlCommand::AdviseSupervisory => ADVISE_EXTRA_LENGTH,
|
||
NmxItemControlCommand::UnAdvise => 0,
|
||
}
|
||
+ PAYLOAD_LENGTH
|
||
}
|
||
|
||
/// Construct a message from the bytes 6..20 of a reference handle.
|
||
/// Mirrors `NmxItemControlMessage.FromReferenceHandle` (`cs:84-101`).
|
||
///
|
||
/// `tail` defaults to [`DEFAULT_TAIL`] (`3`) per `cs:88`.
|
||
#[must_use]
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn from_reference_handle_fields(
|
||
command: NmxItemControlCommand,
|
||
item_correlation_id: [u8; GUID_LENGTH],
|
||
object_id: u16,
|
||
object_signature: u16,
|
||
primitive_id: i16,
|
||
attribute_id: i16,
|
||
property_id: i16,
|
||
attribute_signature: u16,
|
||
attribute_index: i16,
|
||
tail: u32,
|
||
) -> Self {
|
||
Self {
|
||
command,
|
||
item_correlation_id,
|
||
object_id,
|
||
object_signature,
|
||
primitive_id,
|
||
attribute_id,
|
||
property_id,
|
||
attribute_signature,
|
||
attribute_index,
|
||
tail,
|
||
}
|
||
}
|
||
|
||
/// Return a copy with `command = UnAdvise`. Mirrors `ToUnAdvise`
|
||
/// (`NmxItemControlMessage.cs:145-148`).
|
||
#[must_use]
|
||
pub fn to_un_advise(self) -> Self {
|
||
Self {
|
||
command: NmxItemControlCommand::UnAdvise,
|
||
..self
|
||
}
|
||
}
|
||
|
||
/// Return a copy with `command = AdviseSupervisory`. Mirrors
|
||
/// `ToAdviseSupervisory` (`NmxItemControlMessage.cs:150-153`).
|
||
#[must_use]
|
||
pub fn to_advise_supervisory(self) -> Self {
|
||
Self {
|
||
command: NmxItemControlCommand::AdviseSupervisory,
|
||
..self
|
||
}
|
||
}
|
||
|
||
/// Parse an item-control body. Mirrors `NmxItemControlMessage.Parse`
|
||
/// (`cs:38-82`).
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// - [`CodecError::ShortRead`] if the buffer is shorter than the
|
||
/// minimum 37-byte UnAdvise body (`cs:40-43`).
|
||
/// - [`CodecError::UnexpectedOpcode`] if the leading command byte is
|
||
/// neither `0x1f` (AdviseSupervisory) nor `0x21` (UnAdvise) (`cs:45-49`).
|
||
/// This is also what blocks the alias plain-`Advise` from being
|
||
/// accepted on the wire.
|
||
/// - [`CodecError::UnsupportedVersion`] if the version word is not 1
|
||
/// (`cs:51-55`).
|
||
/// - [`CodecError::Decode`] if the buffer length does not match the
|
||
/// per-command expected length (`cs:57-61`).
|
||
pub fn parse(body: &[u8]) -> Result<Self, CodecError> {
|
||
// Minimum length is the UnAdvise body (no advise-extra). Mirrors cs:40.
|
||
let min_len = HEADER_LENGTH + GUID_LENGTH + PAYLOAD_LENGTH;
|
||
if body.len() < min_len {
|
||
return Err(CodecError::ShortRead {
|
||
expected: min_len,
|
||
actual: body.len(),
|
||
});
|
||
}
|
||
|
||
let command = NmxItemControlCommand::from_u8(body[0])?;
|
||
|
||
let version = read_u16_le(body, 1);
|
||
if version != VERSION {
|
||
return Err(CodecError::UnsupportedVersion {
|
||
expected: VERSION,
|
||
actual: version,
|
||
});
|
||
}
|
||
|
||
let expected_length = Self::encoded_length(command);
|
||
if body.len() != expected_length {
|
||
return Err(CodecError::Decode {
|
||
offset: 0,
|
||
reason: "unexpected item-control body length",
|
||
buffer_len: body.len(),
|
||
});
|
||
}
|
||
|
||
let mut offset = HEADER_LENGTH;
|
||
let mut item_correlation_id = [0u8; GUID_LENGTH];
|
||
item_correlation_id.copy_from_slice(&body[offset..offset + GUID_LENGTH]);
|
||
offset += GUID_LENGTH;
|
||
|
||
if command == NmxItemControlCommand::AdviseSupervisory {
|
||
// Skip the 2-byte advise-extra word (cs:66-69). The .NET parser
|
||
// does not retain it; the Rust port mirrors that drop on parse
|
||
// and writes zeros on encode.
|
||
offset += ADVISE_EXTRA_LENGTH;
|
||
}
|
||
|
||
Ok(Self {
|
||
command,
|
||
item_correlation_id,
|
||
object_id: read_u16_le(body, offset),
|
||
object_signature: read_u16_le(body, offset + 2),
|
||
primitive_id: read_i16_le(body, offset + 4),
|
||
attribute_id: read_i16_le(body, offset + 6),
|
||
property_id: read_i16_le(body, offset + 8),
|
||
attribute_signature: read_u16_le(body, offset + 10),
|
||
attribute_index: read_i16_le(body, offset + 12),
|
||
tail: read_u32_le(body, offset + 14),
|
||
})
|
||
}
|
||
|
||
/// Encode to a freshly allocated `Vec<u8>`. Mirrors
|
||
/// `NmxItemControlMessage.Encode` (`cs:121-143`).
|
||
#[must_use]
|
||
pub fn encode(&self) -> Vec<u8> {
|
||
let mut body = vec![0u8; Self::encoded_length(self.command)];
|
||
body[0] = self.command.to_u8();
|
||
write_u16_le(&mut body, 1, VERSION);
|
||
|
||
let mut offset = HEADER_LENGTH;
|
||
body[offset..offset + GUID_LENGTH].copy_from_slice(&self.item_correlation_id);
|
||
offset += GUID_LENGTH;
|
||
|
||
if self.command == NmxItemControlCommand::AdviseSupervisory {
|
||
// Two zero bytes per cs:129-132 — the .NET source advances the
|
||
// offset over already-zeroed buffer space.
|
||
offset += ADVISE_EXTRA_LENGTH;
|
||
}
|
||
|
||
write_u16_le(&mut body, offset, self.object_id);
|
||
write_u16_le(&mut body, offset + 2, self.object_signature);
|
||
write_i16_le(&mut body, offset + 4, self.primitive_id);
|
||
write_i16_le(&mut body, offset + 6, self.attribute_id);
|
||
write_i16_le(&mut body, offset + 8, self.property_id);
|
||
write_u16_le(&mut body, offset + 10, self.attribute_signature);
|
||
write_i16_le(&mut body, offset + 12, self.attribute_index);
|
||
write_u32_le(&mut body, offset + 14, self.tail);
|
||
body
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
fn read_u16_le(bytes: &[u8], offset: usize) -> u16 {
|
||
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
|
||
}
|
||
|
||
#[inline]
|
||
fn read_i16_le(bytes: &[u8], offset: usize) -> i16 {
|
||
i16::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 write_u16_le(bytes: &mut [u8], offset: usize, value: u16) {
|
||
let le = value.to_le_bytes();
|
||
bytes[offset..offset + 2].copy_from_slice(&le);
|
||
}
|
||
|
||
#[inline]
|
||
fn write_i16_le(bytes: &mut [u8], offset: usize, value: i16) {
|
||
let le = value.to_le_bytes();
|
||
bytes[offset..offset + 2].copy_from_slice(&le);
|
||
}
|
||
|
||
#[inline]
|
||
fn write_u32_le(bytes: &mut [u8], offset: usize, value: u32) {
|
||
let le = value.to_le_bytes();
|
||
bytes[offset..offset + 4].copy_from_slice(&le);
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn sample(command: NmxItemControlCommand) -> NmxItemControlMessage {
|
||
NmxItemControlMessage {
|
||
command,
|
||
item_correlation_id: [0x11; GUID_LENGTH],
|
||
object_id: 0x1234,
|
||
object_signature: 0xABCD,
|
||
primitive_id: -1,
|
||
attribute_id: 7,
|
||
property_id: 0,
|
||
attribute_signature: 0xBEEF,
|
||
attribute_index: -1,
|
||
tail: DEFAULT_TAIL,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn encoded_length_matches_dotnet() {
|
||
// cs:30-36: AdviseSupervisory = 3+16+2+18 = 39, UnAdvise = 3+16+18 = 37.
|
||
assert_eq!(
|
||
NmxItemControlMessage::encoded_length(NmxItemControlCommand::AdviseSupervisory),
|
||
39
|
||
);
|
||
assert_eq!(
|
||
NmxItemControlMessage::encoded_length(NmxItemControlCommand::UnAdvise),
|
||
37
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn round_trip_advise_supervisory() {
|
||
let msg = sample(NmxItemControlCommand::AdviseSupervisory);
|
||
let encoded = msg.encode();
|
||
assert_eq!(encoded.len(), 39);
|
||
let decoded = NmxItemControlMessage::parse(&encoded).unwrap();
|
||
assert_eq!(msg, decoded);
|
||
}
|
||
|
||
#[test]
|
||
fn round_trip_un_advise() {
|
||
let msg = sample(NmxItemControlCommand::UnAdvise);
|
||
let encoded = msg.encode();
|
||
assert_eq!(encoded.len(), 37);
|
||
let decoded = NmxItemControlMessage::parse(&encoded).unwrap();
|
||
assert_eq!(msg, decoded);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_plain_advise_alias() {
|
||
// The .NET enum aliases `Advise = 0x1f = AdviseSupervisory`, so on
|
||
// the wire the leading byte 0x1f always means AdviseSupervisory.
|
||
// There is *no* 37-byte plain-Advise shape; a 37-byte body that
|
||
// starts with 0x1f must be rejected because the per-command length
|
||
// check (cs:57-61) demands 39 bytes for 0x1f.
|
||
let mut bogus = vec![0u8; 37];
|
||
bogus[0] = 0x1f;
|
||
write_u16_le(&mut bogus, 1, VERSION);
|
||
let err = NmxItemControlMessage::parse(&bogus).unwrap_err();
|
||
assert!(
|
||
matches!(err, CodecError::Decode { .. }),
|
||
"expected length-mismatch decode error, got {err:?}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_unknown_command_opcode() {
|
||
// Leading byte that is neither 0x1f nor 0x21 — cs:46-49.
|
||
let mut bogus = vec![0u8; 37];
|
||
bogus[0] = 0x42;
|
||
write_u16_le(&mut bogus, 1, VERSION);
|
||
let err = NmxItemControlMessage::parse(&bogus).unwrap_err();
|
||
assert!(matches!(err, CodecError::UnexpectedOpcode(0x42)));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_wrong_length_buffer_advise() {
|
||
// 39 bytes is right for AdviseSupervisory; 38 is wrong.
|
||
let msg = sample(NmxItemControlCommand::AdviseSupervisory);
|
||
let mut encoded = msg.encode();
|
||
encoded.pop();
|
||
let err = NmxItemControlMessage::parse(&encoded).unwrap_err();
|
||
// 38 bytes still passes the 37-byte minimum, so we hit the
|
||
// per-command length mismatch (cs:57-61) — Decode.
|
||
assert!(matches!(err, CodecError::Decode { .. }));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_wrong_length_buffer_un_advise() {
|
||
// 36 bytes is below the 37-byte UnAdvise minimum (cs:40).
|
||
let err = NmxItemControlMessage::parse(&[0u8; 36]).unwrap_err();
|
||
assert!(matches!(err, CodecError::ShortRead { .. }));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_oversized_un_advise() {
|
||
// 38 bytes with leading 0x21 — UnAdvise demands exactly 37.
|
||
let mut bogus = vec![0u8; 38];
|
||
bogus[0] = 0x21;
|
||
write_u16_le(&mut bogus, 1, VERSION);
|
||
let err = NmxItemControlMessage::parse(&bogus).unwrap_err();
|
||
assert!(matches!(err, CodecError::Decode { .. }));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_wrong_version() {
|
||
let msg = sample(NmxItemControlCommand::UnAdvise);
|
||
let mut encoded = msg.encode();
|
||
write_u16_le(&mut encoded, 1, 2);
|
||
let err = NmxItemControlMessage::parse(&encoded).unwrap_err();
|
||
assert!(matches!(err, CodecError::UnsupportedVersion { .. }));
|
||
}
|
||
|
||
#[test]
|
||
fn guid_round_trips_byte_identical() {
|
||
// Use a distinctive byte pattern so a re-shuffle would be obvious.
|
||
let guid: [u8; 16] = [
|
||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
|
||
0xee, 0xff,
|
||
];
|
||
let msg = NmxItemControlMessage {
|
||
item_correlation_id: guid,
|
||
..sample(NmxItemControlCommand::AdviseSupervisory)
|
||
};
|
||
let encoded = msg.encode();
|
||
// GUID lives at offset 3..19 (after cmd+version) per cs:63-65.
|
||
assert_eq!(&encoded[3..19], &guid);
|
||
let decoded = NmxItemControlMessage::parse(&encoded).unwrap();
|
||
assert_eq!(decoded.item_correlation_id, guid);
|
||
}
|
||
|
||
#[test]
|
||
fn guid_round_trips_known_pattern() {
|
||
// The brief calls out `[0x11; 16]` as a sanity vector.
|
||
let guid = [0x11u8; 16];
|
||
let msg = NmxItemControlMessage {
|
||
item_correlation_id: guid,
|
||
..sample(NmxItemControlCommand::UnAdvise)
|
||
};
|
||
let encoded = msg.encode();
|
||
assert_eq!(&encoded[3..19], &guid);
|
||
let decoded = NmxItemControlMessage::parse(&encoded).unwrap();
|
||
assert_eq!(decoded.item_correlation_id, [0x11; 16]);
|
||
}
|
||
|
||
#[test]
|
||
fn default_tail_is_three() {
|
||
// cs:88 — `uint tail = 3`.
|
||
assert_eq!(DEFAULT_TAIL, 3);
|
||
let msg = NmxItemControlMessage::from_reference_handle_fields(
|
||
NmxItemControlCommand::AdviseSupervisory,
|
||
[0x11; 16],
|
||
1,
|
||
2,
|
||
3,
|
||
4,
|
||
5,
|
||
6,
|
||
7,
|
||
DEFAULT_TAIL,
|
||
);
|
||
let encoded = msg.encode();
|
||
// Tail u32 lives at the last 4 bytes of the body.
|
||
let n = encoded.len();
|
||
assert_eq!(&encoded[n - 4..], &3u32.to_le_bytes());
|
||
}
|
||
|
||
#[test]
|
||
fn advise_supervisory_extra_bytes_are_zero_on_encode() {
|
||
// cs:129-132: the 2-byte advise-extra word at offset 19..21 is
|
||
// skipped (left as zero in the freshly-allocated buffer).
|
||
let msg = sample(NmxItemControlCommand::AdviseSupervisory);
|
||
let encoded = msg.encode();
|
||
assert_eq!(&encoded[19..21], &[0x00, 0x00]);
|
||
}
|
||
|
||
#[test]
|
||
fn handle_projection_offsets_advise_supervisory() {
|
||
// For AdviseSupervisory the handle projection starts at offset 21
|
||
// (3 + 16 + 2). Verify object_id 0x1234 lands as 34 12 there.
|
||
let msg = NmxItemControlMessage {
|
||
object_id: 0x1234,
|
||
..sample(NmxItemControlCommand::AdviseSupervisory)
|
||
};
|
||
let encoded = msg.encode();
|
||
assert_eq!(encoded[21], 0x34);
|
||
assert_eq!(encoded[22], 0x12);
|
||
}
|
||
|
||
#[test]
|
||
fn handle_projection_offsets_un_advise() {
|
||
// For UnAdvise the handle projection starts at offset 19 (3 + 16).
|
||
let msg = NmxItemControlMessage {
|
||
object_id: 0x1234,
|
||
..sample(NmxItemControlCommand::UnAdvise)
|
||
};
|
||
let encoded = msg.encode();
|
||
assert_eq!(encoded[19], 0x34);
|
||
assert_eq!(encoded[20], 0x12);
|
||
}
|
||
|
||
#[test]
|
||
fn version_word_is_one_le() {
|
||
let encoded = sample(NmxItemControlCommand::UnAdvise).encode();
|
||
assert_eq!(encoded[1], 0x01);
|
||
assert_eq!(encoded[2], 0x00);
|
||
}
|
||
|
||
#[test]
|
||
fn command_byte_round_trips() {
|
||
let advise = sample(NmxItemControlCommand::AdviseSupervisory).encode();
|
||
let unadvise = sample(NmxItemControlCommand::UnAdvise).encode();
|
||
assert_eq!(advise[0], 0x1f);
|
||
assert_eq!(unadvise[0], 0x21);
|
||
}
|
||
|
||
#[test]
|
||
fn to_un_advise_and_back() {
|
||
// Mirrors cs:145-153 — `with` updates only the command.
|
||
let advise = sample(NmxItemControlCommand::AdviseSupervisory);
|
||
let unadvise = advise.to_un_advise();
|
||
assert_eq!(unadvise.command, NmxItemControlCommand::UnAdvise);
|
||
// All other fields preserved.
|
||
assert_eq!(advise.item_correlation_id, unadvise.item_correlation_id);
|
||
assert_eq!(advise.object_id, unadvise.object_id);
|
||
assert_eq!(advise.tail, unadvise.tail);
|
||
let again = unadvise.to_advise_supervisory();
|
||
assert_eq!(again.command, NmxItemControlCommand::AdviseSupervisory);
|
||
assert_eq!(again, advise);
|
||
}
|
||
}
|