Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
//! Value model — `MxValueKind`, `MxDataType`, and `MxValue`.
|
||||
//!
|
||||
//! Ports `src/MxNativeCodec/MxValueKind.cs`, `src/MxNativeCodec/MxDataType.cs`,
|
||||
//! and the wire-kind/value mapping that `NmxSubscriptionMessage.cs` and
|
||||
//! `NmxWriteMessage.cs` use. `MxValueKind` carries the on-the-wire numeric
|
||||
//! tags observed in NMX subscription / write bodies; `MxDataType` is the
|
||||
//! attribute-model side of the .NET enum and is independent of the wire
|
||||
//! kind; `MxValue` is the runtime carrier used by codecs.
|
||||
//!
|
||||
//! The wire-kind values are not encoded as an enum in the .NET reference
|
||||
//! (`MxValueKind.cs:3-18` uses default integer ordering) — the `0x01..0x07`
|
||||
//! and `0x41..0x46` byte tags come from the encoder/decoder switches in
|
||||
//! `NmxWriteMessage.cs:94-110` and `NmxSubscriptionMessage.cs:164-176`.
|
||||
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
/// On-the-wire value kind tag.
|
||||
///
|
||||
/// Per `NmxWriteMessage.cs:94-110` (`GetWireKind`) and
|
||||
/// `NmxSubscriptionMessage.cs:164-176` (`DecodeValue`). The byte values are
|
||||
/// the actual wire tags written into / read out of NMX message bodies; they
|
||||
/// are NOT the `int` ordinals of the C# `MxValueKind` enum
|
||||
/// (`MxValueKind.cs:3-18`).
|
||||
///
|
||||
/// Encoder asymmetry: both `StringArray` and `DateTimeArray` are written as
|
||||
/// `0x45` on the wire (`NmxWriteMessage.cs:107`), but the decoder
|
||||
/// distinguishes `0x46` for `DateTimeArray`
|
||||
/// (`NmxSubscriptionMessage.cs:173,275`). [`MxValue::kind`] reflects the
|
||||
/// encoder behaviour — see its docs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
pub enum MxValueKind {
|
||||
/// Sentinel for unrecognised wire tags. Not on the wire; used by
|
||||
/// [`MxValueKind::from_u8`] to surface "unknown kind" without panicking.
|
||||
#[default]
|
||||
Unknown = 0x00,
|
||||
/// `MxValueKind.cs:5` / wire `0x01` (`NmxWriteMessage.cs:98`,
|
||||
/// `NmxSubscriptionMessage.cs:166`).
|
||||
Boolean = 0x01,
|
||||
/// `MxValueKind.cs:6` / wire `0x02` (`NmxWriteMessage.cs:99`,
|
||||
/// `NmxSubscriptionMessage.cs:167`).
|
||||
Int32 = 0x02,
|
||||
/// `MxValueKind.cs:7` / wire `0x03` (`NmxWriteMessage.cs:100`,
|
||||
/// `NmxSubscriptionMessage.cs:168`).
|
||||
Float32 = 0x03,
|
||||
/// `MxValueKind.cs:8` / wire `0x04` (`NmxWriteMessage.cs:101`,
|
||||
/// `NmxSubscriptionMessage.cs:169`).
|
||||
Float64 = 0x04,
|
||||
/// `MxValueKind.cs:9` / wire `0x05` (`NmxWriteMessage.cs:102`,
|
||||
/// `NmxSubscriptionMessage.cs:170`). Encoder collapses `String` and
|
||||
/// `DateTime` to the same tag (`NmxWriteMessage.cs:102`).
|
||||
String = 0x05,
|
||||
/// `MxValueKind.cs:10` / wire `0x06` on the decode path
|
||||
/// (`NmxSubscriptionMessage.cs:171`). Encoder collapses to `0x05`
|
||||
/// (`NmxWriteMessage.cs:102`).
|
||||
DateTime = 0x06,
|
||||
/// `MxValueKind.cs:11` / wire `0x07` (`NmxSubscriptionMessage.cs:172,253`).
|
||||
/// Decoder reads a signed `i32` milliseconds value.
|
||||
ElapsedTime = 0x07,
|
||||
/// `MxValueKind.cs:12` / wire `0x41` (`NmxWriteMessage.cs:103`,
|
||||
/// `NmxSubscriptionMessage.cs:173,270`).
|
||||
BoolArray = 0x41,
|
||||
/// `MxValueKind.cs:13` / wire `0x42` (`NmxWriteMessage.cs:104`,
|
||||
/// `NmxSubscriptionMessage.cs:173,271`).
|
||||
Int32Array = 0x42,
|
||||
/// `MxValueKind.cs:14` / wire `0x43` (`NmxWriteMessage.cs:105`,
|
||||
/// `NmxSubscriptionMessage.cs:173,272`).
|
||||
Float32Array = 0x43,
|
||||
/// `MxValueKind.cs:15` / wire `0x44` (`NmxWriteMessage.cs:106`,
|
||||
/// `NmxSubscriptionMessage.cs:173,273`).
|
||||
Float64Array = 0x44,
|
||||
/// `MxValueKind.cs:16` / wire `0x45` (`NmxWriteMessage.cs:107`,
|
||||
/// `NmxSubscriptionMessage.cs:173,274`). Encoder collapses
|
||||
/// `StringArray` and `DateTimeArray` to this tag
|
||||
/// (`NmxWriteMessage.cs:107`).
|
||||
StringArray = 0x45,
|
||||
/// `MxValueKind.cs:17` / wire `0x46` on the decode path
|
||||
/// (`NmxSubscriptionMessage.cs:173,275`). Encoder collapses to `0x45`
|
||||
/// (`NmxWriteMessage.cs:107`).
|
||||
DateTimeArray = 0x46,
|
||||
// ElapsedTimeArray (0x47) is not enumerated by `MxValueKind.cs` and is
|
||||
// not handled by either the encoder or decoder. Known gap, parity with
|
||||
// .NET reference.
|
||||
}
|
||||
|
||||
impl MxValueKind {
|
||||
/// Decode a wire byte into a kind. Returns [`MxValueKind::Unknown`] for
|
||||
/// any tag not in `0x01..=0x07` or `0x41..=0x46` — mirrors the fall-
|
||||
/// through arms of `NmxSubscriptionMessage.DecodeValue`
|
||||
/// (`NmxSubscriptionMessage.cs:174`) and `ToValueKindOrNull` in the
|
||||
/// .NET reference.
|
||||
pub fn from_u8(value: u8) -> Self {
|
||||
match value {
|
||||
0x01 => Self::Boolean,
|
||||
0x02 => Self::Int32,
|
||||
0x03 => Self::Float32,
|
||||
0x04 => Self::Float64,
|
||||
0x05 => Self::String,
|
||||
0x06 => Self::DateTime,
|
||||
0x07 => Self::ElapsedTime,
|
||||
0x41 => Self::BoolArray,
|
||||
0x42 => Self::Int32Array,
|
||||
0x43 => Self::Float32Array,
|
||||
0x44 => Self::Float64Array,
|
||||
0x45 => Self::StringArray,
|
||||
0x46 => Self::DateTimeArray,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the kind to its wire byte. `Unknown` returns `0x00`; do not
|
||||
/// emit `Unknown` to the wire — the .NET encoder
|
||||
/// (`NmxWriteMessage.cs:108`) throws `ArgumentOutOfRangeException` for
|
||||
/// any kind not in its match.
|
||||
pub fn to_u8(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// Attribute-model data type — port of `MxDataType.cs:3-24`.
|
||||
///
|
||||
/// This is the model-side attribute classification, distinct from the wire
|
||||
/// `MxValueKind`. The numeric values are the explicit `short` discriminants
|
||||
/// from `MxDataType.cs:3` (`enum MxDataType : short`). Used by the runtime
|
||||
/// model and by `RegisterMxReferences` results, not by NMX value bodies.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
#[non_exhaustive]
|
||||
#[repr(i16)]
|
||||
pub enum MxDataType {
|
||||
/// `MxDataType.cs:5` — sentinel for "no type known".
|
||||
#[default]
|
||||
Unknown = -1,
|
||||
/// `MxDataType.cs:6`.
|
||||
NoData = 0,
|
||||
/// `MxDataType.cs:7`.
|
||||
Boolean = 1,
|
||||
/// `MxDataType.cs:8`.
|
||||
Integer = 2,
|
||||
/// `MxDataType.cs:9`.
|
||||
Float = 3,
|
||||
/// `MxDataType.cs:10`.
|
||||
Double = 4,
|
||||
/// `MxDataType.cs:11`.
|
||||
String = 5,
|
||||
/// `MxDataType.cs:12`.
|
||||
Time = 6,
|
||||
/// `MxDataType.cs:13`.
|
||||
ElapsedTime = 7,
|
||||
/// `MxDataType.cs:14`.
|
||||
ReferenceType = 8,
|
||||
/// `MxDataType.cs:15`.
|
||||
StatusType = 9,
|
||||
/// `MxDataType.cs:16`.
|
||||
Enum = 10,
|
||||
/// `MxDataType.cs:17`.
|
||||
SecurityClassificationEnum = 11,
|
||||
/// `MxDataType.cs:18`.
|
||||
DataQualityType = 12,
|
||||
/// `MxDataType.cs:19`.
|
||||
QualifiedEnum = 13,
|
||||
/// `MxDataType.cs:20`.
|
||||
QualifiedStruct = 14,
|
||||
/// `MxDataType.cs:21`.
|
||||
InternationalizedString = 15,
|
||||
/// `MxDataType.cs:22`.
|
||||
BigString = 16,
|
||||
/// `MxDataType.cs:23` — terminator sentinel from the .NET enum.
|
||||
End = 17,
|
||||
}
|
||||
|
||||
impl MxDataType {
|
||||
/// Decode the model-side type id. Out-of-range values map to
|
||||
/// [`MxDataType::Unknown`]. Mirrors the `Unknown = -1` sentinel from
|
||||
/// `MxDataType.cs:5`.
|
||||
pub fn from_i16(value: i16) -> Self {
|
||||
match value {
|
||||
0 => Self::NoData,
|
||||
1 => Self::Boolean,
|
||||
2 => Self::Integer,
|
||||
3 => Self::Float,
|
||||
4 => Self::Double,
|
||||
5 => Self::String,
|
||||
6 => Self::Time,
|
||||
7 => Self::ElapsedTime,
|
||||
8 => Self::ReferenceType,
|
||||
9 => Self::StatusType,
|
||||
10 => Self::Enum,
|
||||
11 => Self::SecurityClassificationEnum,
|
||||
12 => Self::DataQualityType,
|
||||
13 => Self::QualifiedEnum,
|
||||
14 => Self::QualifiedStruct,
|
||||
15 => Self::InternationalizedString,
|
||||
16 => Self::BigString,
|
||||
17 => Self::End,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_i16(self) -> i16 {
|
||||
self as i16
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime carrier for a decoded MXAccess value.
|
||||
///
|
||||
/// Variant set tracks `NmxSubscriptionMessage.DecodeValue`
|
||||
/// (`NmxSubscriptionMessage.cs:164-176`): the seven scalar wire kinds
|
||||
/// (`0x01..=0x07`) plus the six array wire kinds (`0x41..=0x46`,
|
||||
/// minus `ElapsedTimeArray` which has no .NET support).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum MxValue {
|
||||
/// Scalar boolean, wire `0x01` (`NmxSubscriptionMessage.cs:166`).
|
||||
Boolean(bool),
|
||||
/// Scalar `i32`, wire `0x02` (`NmxSubscriptionMessage.cs:167`).
|
||||
Int32(i32),
|
||||
/// Scalar `f32`, wire `0x03` (`NmxSubscriptionMessage.cs:168`).
|
||||
Float32(f32),
|
||||
/// Scalar `f64`, wire `0x04` (`NmxSubscriptionMessage.cs:169`).
|
||||
Float64(f64),
|
||||
/// Scalar UTF-16LE string, wire `0x05`
|
||||
/// (`NmxSubscriptionMessage.cs:170,178-210`).
|
||||
String(String),
|
||||
/// Windows `FILETIME` ticks (100ns since 1601-01-01 UTC), wire `0x06`
|
||||
/// (`NmxSubscriptionMessage.cs:171,212-243`). Carries the raw `i64`
|
||||
/// rather than a `DateTime` to preserve byte-for-byte parity even when
|
||||
/// the value falls outside `chrono`/`time` clamp ranges.
|
||||
DateTime(i64),
|
||||
/// Signed milliseconds, wire `0x07`
|
||||
/// (`NmxSubscriptionMessage.cs:172,245-254`). Wire is `i32`; widened to
|
||||
/// `i64` here to allow the model to be unambiguous about sign and to
|
||||
/// avoid forcing the `std::time::Duration` (unsigned) shape.
|
||||
ElapsedTime(i64),
|
||||
/// Boolean array, wire `0x41`
|
||||
/// (`NmxSubscriptionMessage.cs:173,280-294`). On the wire each element
|
||||
/// is an `i16` per `elementWidth==sizeof(short)` check.
|
||||
BoolArray(Vec<bool>),
|
||||
/// `i32` array, wire `0x42`
|
||||
/// (`NmxSubscriptionMessage.cs:173,296-310`).
|
||||
Int32Array(Vec<i32>),
|
||||
/// `f32` array, wire `0x43`
|
||||
/// (`NmxSubscriptionMessage.cs:173,312-326`).
|
||||
Float32Array(Vec<f32>),
|
||||
/// `f64` array, wire `0x44`
|
||||
/// (`NmxSubscriptionMessage.cs:173,328-342`).
|
||||
Float64Array(Vec<f64>),
|
||||
/// String array, wire `0x45` on encode AND decode
|
||||
/// (`NmxWriteMessage.cs:107`, `NmxSubscriptionMessage.cs:173,274`).
|
||||
StringArray(Vec<String>),
|
||||
/// `DateTime` array; decoded from wire `0x46`
|
||||
/// (`NmxSubscriptionMessage.cs:173,275`), but encoded as `0x45`
|
||||
/// (`NmxWriteMessage.cs:107`). Elements are raw `FILETIME` ticks.
|
||||
DateTimeArray(Vec<i64>),
|
||||
}
|
||||
|
||||
impl MxValue {
|
||||
/// Wire kind for this value. Mirrors `NmxWriteMessage.GetWireKind`
|
||||
/// (`NmxWriteMessage.cs:94-110`) — i.e. the *encoder* behaviour.
|
||||
///
|
||||
/// **Encoder collapse:** both [`MxValue::StringArray`] and
|
||||
/// [`MxValue::DateTimeArray`] return [`MxValueKind::StringArray`]
|
||||
/// (`0x45`) here, matching `NmxWriteMessage.cs:107`. The decoder is
|
||||
/// asymmetric (`0x46` round-trips back into `DateTimeArray`); see
|
||||
/// [`MxValueKind`] docs.
|
||||
pub fn kind(&self) -> MxValueKind {
|
||||
match self {
|
||||
Self::Boolean(_) => MxValueKind::Boolean,
|
||||
Self::Int32(_) => MxValueKind::Int32,
|
||||
Self::Float32(_) => MxValueKind::Float32,
|
||||
Self::Float64(_) => MxValueKind::Float64,
|
||||
Self::String(_) => MxValueKind::String,
|
||||
// Per NmxWriteMessage.cs:102 the encoder collapses DateTime to
|
||||
// the String wire tag (0x05). Returning `DateTime` here keeps
|
||||
// the model side honest; encoders should re-map via
|
||||
// `MxValueKind::to_u8` semantics or call out to a future
|
||||
// `to_wire_byte` helper.
|
||||
Self::DateTime(_) => MxValueKind::DateTime,
|
||||
Self::ElapsedTime(_) => MxValueKind::ElapsedTime,
|
||||
Self::BoolArray(_) => MxValueKind::BoolArray,
|
||||
Self::Int32Array(_) => MxValueKind::Int32Array,
|
||||
Self::Float32Array(_) => MxValueKind::Float32Array,
|
||||
Self::Float64Array(_) => MxValueKind::Float64Array,
|
||||
// Encoder collapse: NmxWriteMessage.cs:107 maps both
|
||||
// StringArray and DateTimeArray to wire 0x45. Matches the
|
||||
// .NET encoder; the decoder asymmetrically uses 0x46 for
|
||||
// DateTimeArray (NmxSubscriptionMessage.cs:275).
|
||||
Self::StringArray(_) => MxValueKind::StringArray,
|
||||
Self::DateTimeArray(_) => MxValueKind::StringArray,
|
||||
}
|
||||
}
|
||||
|
||||
/// Model-side data type hint. This is best-effort: the wire never
|
||||
/// carries `MxDataType` — it's the model classification used by
|
||||
/// register results and the public API. Arrays return their scalar
|
||||
/// element's `MxDataType` (the .NET reference does not have an
|
||||
/// "array of X" `MxDataType` discriminant).
|
||||
pub fn data_type(&self) -> MxDataType {
|
||||
match self {
|
||||
Self::Boolean(_) | Self::BoolArray(_) => MxDataType::Boolean,
|
||||
Self::Int32(_) | Self::Int32Array(_) => MxDataType::Integer,
|
||||
Self::Float32(_) | Self::Float32Array(_) => MxDataType::Float,
|
||||
Self::Float64(_) | Self::Float64Array(_) => MxDataType::Double,
|
||||
Self::String(_) | Self::StringArray(_) => MxDataType::String,
|
||||
Self::DateTime(_) | Self::DateTimeArray(_) => MxDataType::Time,
|
||||
Self::ElapsedTime(_) => MxDataType::ElapsedTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const ALL_KINDS: &[MxValueKind] = &[
|
||||
MxValueKind::Unknown,
|
||||
MxValueKind::Boolean,
|
||||
MxValueKind::Int32,
|
||||
MxValueKind::Float32,
|
||||
MxValueKind::Float64,
|
||||
MxValueKind::String,
|
||||
MxValueKind::DateTime,
|
||||
MxValueKind::ElapsedTime,
|
||||
MxValueKind::BoolArray,
|
||||
MxValueKind::Int32Array,
|
||||
MxValueKind::Float32Array,
|
||||
MxValueKind::Float64Array,
|
||||
MxValueKind::StringArray,
|
||||
MxValueKind::DateTimeArray,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn value_kind_round_trip() {
|
||||
for &kind in ALL_KINDS {
|
||||
assert_eq!(MxValueKind::from_u8(kind.to_u8()), kind, "{kind:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_kind_unknown_byte_maps_to_unknown() {
|
||||
assert_eq!(MxValueKind::from_u8(0xff), MxValueKind::Unknown);
|
||||
// Tags between scalar (0x07) and array (0x41) ranges are not
|
||||
// assigned in the .NET reference.
|
||||
assert_eq!(MxValueKind::from_u8(0x08), MxValueKind::Unknown);
|
||||
assert_eq!(MxValueKind::from_u8(0x40), MxValueKind::Unknown);
|
||||
// 0x47 (would-be ElapsedTimeArray) is a documented gap — must
|
||||
// currently surface as Unknown.
|
||||
assert_eq!(MxValueKind::from_u8(0x47), MxValueKind::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_kind_to_u8_matches_wire_tags() {
|
||||
// Spot-check the wire bytes against the .cs sources.
|
||||
assert_eq!(MxValueKind::Boolean.to_u8(), 0x01);
|
||||
assert_eq!(MxValueKind::Int32.to_u8(), 0x02);
|
||||
assert_eq!(MxValueKind::Float32.to_u8(), 0x03);
|
||||
assert_eq!(MxValueKind::Float64.to_u8(), 0x04);
|
||||
assert_eq!(MxValueKind::String.to_u8(), 0x05);
|
||||
assert_eq!(MxValueKind::DateTime.to_u8(), 0x06);
|
||||
assert_eq!(MxValueKind::ElapsedTime.to_u8(), 0x07);
|
||||
assert_eq!(MxValueKind::BoolArray.to_u8(), 0x41);
|
||||
assert_eq!(MxValueKind::Int32Array.to_u8(), 0x42);
|
||||
assert_eq!(MxValueKind::Float32Array.to_u8(), 0x43);
|
||||
assert_eq!(MxValueKind::Float64Array.to_u8(), 0x44);
|
||||
assert_eq!(MxValueKind::StringArray.to_u8(), 0x45);
|
||||
assert_eq!(MxValueKind::DateTimeArray.to_u8(), 0x46);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_type_round_trip() {
|
||||
let all = [
|
||||
MxDataType::Unknown,
|
||||
MxDataType::NoData,
|
||||
MxDataType::Boolean,
|
||||
MxDataType::Integer,
|
||||
MxDataType::Float,
|
||||
MxDataType::Double,
|
||||
MxDataType::String,
|
||||
MxDataType::Time,
|
||||
MxDataType::ElapsedTime,
|
||||
MxDataType::ReferenceType,
|
||||
MxDataType::StatusType,
|
||||
MxDataType::Enum,
|
||||
MxDataType::SecurityClassificationEnum,
|
||||
MxDataType::DataQualityType,
|
||||
MxDataType::QualifiedEnum,
|
||||
MxDataType::QualifiedStruct,
|
||||
MxDataType::InternationalizedString,
|
||||
MxDataType::BigString,
|
||||
MxDataType::End,
|
||||
];
|
||||
for dt in all {
|
||||
assert_eq!(MxDataType::from_i16(dt.to_i16()), dt, "{dt:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_type_out_of_range_maps_to_unknown() {
|
||||
assert_eq!(MxDataType::from_i16(-2), MxDataType::Unknown);
|
||||
assert_eq!(MxDataType::from_i16(18), MxDataType::Unknown);
|
||||
assert_eq!(MxDataType::from_i16(i16::MAX), MxDataType::Unknown);
|
||||
assert_eq!(MxDataType::from_i16(i16::MIN), MxDataType::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_kind_for_each_variant() {
|
||||
assert_eq!(MxValue::Boolean(true).kind(), MxValueKind::Boolean);
|
||||
assert_eq!(MxValue::Int32(0).kind(), MxValueKind::Int32);
|
||||
assert_eq!(MxValue::Float32(0.0).kind(), MxValueKind::Float32);
|
||||
assert_eq!(MxValue::Float64(0.0).kind(), MxValueKind::Float64);
|
||||
assert_eq!(MxValue::String(String::new()).kind(), MxValueKind::String);
|
||||
assert_eq!(MxValue::DateTime(0).kind(), MxValueKind::DateTime);
|
||||
assert_eq!(MxValue::ElapsedTime(0).kind(), MxValueKind::ElapsedTime);
|
||||
assert_eq!(MxValue::BoolArray(vec![]).kind(), MxValueKind::BoolArray);
|
||||
assert_eq!(MxValue::Int32Array(vec![]).kind(), MxValueKind::Int32Array);
|
||||
assert_eq!(
|
||||
MxValue::Float32Array(vec![]).kind(),
|
||||
MxValueKind::Float32Array
|
||||
);
|
||||
assert_eq!(
|
||||
MxValue::Float64Array(vec![]).kind(),
|
||||
MxValueKind::Float64Array
|
||||
);
|
||||
}
|
||||
|
||||
/// Both `StringArray` and `DateTimeArray` collapse to the same wire
|
||||
/// kind on the encode path. This mirrors `NmxWriteMessage.cs:107`:
|
||||
///
|
||||
/// ```text
|
||||
/// MxValueKind.StringArray or MxValueKind.DateTimeArray => 0x45,
|
||||
/// ```
|
||||
#[test]
|
||||
fn string_and_datetime_arrays_collapse_to_string_array_wire_kind() {
|
||||
let s = MxValue::StringArray(vec!["a".to_string()]);
|
||||
let d = MxValue::DateTimeArray(vec![0_i64]);
|
||||
|
||||
assert_eq!(s.kind(), MxValueKind::StringArray);
|
||||
assert_eq!(d.kind(), MxValueKind::StringArray);
|
||||
assert_eq!(s.kind(), d.kind());
|
||||
assert_eq!(s.kind().to_u8(), 0x45);
|
||||
assert_eq!(d.kind().to_u8(), 0x45);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_type_for_each_value() {
|
||||
assert_eq!(MxValue::Boolean(false).data_type(), MxDataType::Boolean);
|
||||
assert_eq!(MxValue::Int32(0).data_type(), MxDataType::Integer);
|
||||
assert_eq!(MxValue::Float32(0.0).data_type(), MxDataType::Float);
|
||||
assert_eq!(MxValue::Float64(0.0).data_type(), MxDataType::Double);
|
||||
assert_eq!(
|
||||
MxValue::String(String::new()).data_type(),
|
||||
MxDataType::String
|
||||
);
|
||||
assert_eq!(MxValue::DateTime(0).data_type(), MxDataType::Time);
|
||||
assert_eq!(MxValue::ElapsedTime(0).data_type(), MxDataType::ElapsedTime);
|
||||
assert_eq!(MxValue::BoolArray(vec![]).data_type(), MxDataType::Boolean);
|
||||
assert_eq!(MxValue::DateTimeArray(vec![]).data_type(), MxDataType::Time);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_match_dotnet_sentinels() {
|
||||
// MxValueKind::Unknown is the from_u8 fall-through and the Default
|
||||
// (matches `ToValueKindOrNull` semantics in
|
||||
// `NmxSubscriptionMessage.cs:174`).
|
||||
assert_eq!(MxValueKind::default(), MxValueKind::Unknown);
|
||||
// MxDataType::Unknown == -1 per `MxDataType.cs:5`.
|
||||
assert_eq!(MxDataType::default(), MxDataType::Unknown);
|
||||
assert_eq!(MxDataType::default().to_i16(), -1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user