1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! Version-carrying NatNet decoding and checked re-encoding.
use crate::message;
use crate::{Error, FrameOfData, Message, ModelDef, ParsePolicy, Result, ServerInfo, Version};
/// Decodes messages using a supported protocol layout and parsing policy.
/// Construct from [`ServerInfo`]'s reported version.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Decoder {
version: Version,
policy: ParsePolicy,
}
impl Decoder {
/// Selects a supported layout with strict validation.
/// # Errors
/// Returns [`Error::UnsupportedVersion`] for an unlisted major/minor pair.
pub fn new(version: Version) -> Result<Self> {
version.validate()?;
Ok(Self {
version,
policy: ParsePolicy::Strict,
})
}
/// Selects the explicit parsing policy while retaining the same layout.
#[must_use]
pub const fn with_policy(mut self, policy: ParsePolicy) -> Self {
self.policy = policy;
self
}
/// The reported version whose layout this decoder uses.
#[must_use]
pub const fn version(self) -> Version {
self.version
}
/// The validation policy used for known messages.
#[must_use]
pub const fn policy(self) -> ParsePolicy {
self.policy
}
/// Decodes one datagram, retaining the layout for subsequent encoding.
/// Unknown message IDs remain opaque. The outer declared length remains
/// telemetry-only under both policies.
/// # Errors
/// Returns header, field or strict-validation errors for known messages.
pub fn parse(self, datagram: &[u8]) -> Result<DecodedMessage> {
let (header, payload) = message::split(datagram)?;
let message = match header.message_id {
message::NAT_SERVERINFO => {
Message::ServerInfo(ServerInfo::parse_payload_with_policy(payload, self.policy)?)
}
message::NAT_MODELDEF => Message::ModelDef(ModelDef::parse_payload_with_policy(
payload,
self.version,
self.policy,
)?),
message::NAT_FRAMEOFDATA => Message::FrameOfData(Box::new(
FrameOfData::parse_payload_with_policy(payload, self.version, self.policy)?,
)),
id => Message::Other {
id,
payload: payload.to_vec(),
},
};
Ok(DecodedMessage {
message,
version: self.version,
})
}
}
/// A decoded message paired with the layout used to interpret it.
#[derive(Debug, Clone, PartialEq)]
pub struct DecodedMessage {
message: Message,
version: Version,
}
impl DecodedMessage {
/// The decoded message. Borrowing keeps the source-layout pairing.
#[must_use]
pub const fn message(&self) -> &Message {
&self.message
}
/// The version selected by the decoder.
#[must_use]
pub const fn version(&self) -> Version {
self.version
}
/// Returns the message without its source-layout pairing.
#[must_use]
pub fn into_message(self) -> Message {
self.message
}
/// Re-encodes using the original source layout.
///
/// # Errors
///
/// Returns checked encoding errors. A decoded message can still fail, e.g.
/// a payload over the 65 535-byte wire limit
/// ([`Error::LengthOverflow`](crate::Error::LengthOverflow)), or a name
/// that lossy UTF-8 replacement widened past its field. Extensions kept
/// by [`ParsePolicy::Compatible`](crate::ParsePolicy::Compatible), such as
/// residue inside a sized frame section, fail checked encoding.
pub fn to_datagram(&self) -> Result<Vec<u8>> {
self.to_datagram_for(self.version)
}
/// Explicitly re-encodes in a supported destination layout.
///
/// Cross-layout opaque frame sections, MODELDEF rigid-body tails
/// (including skeleton and asset bodies) and [`DataSet::Other`](crate::DataSet::Other)
/// datasets require reconstruction through [`Self::into_message`]. The
/// destination encoder refuses unsupported assets or precision fields.
/// # Errors
/// Returns unsupported-version, opaque-conversion or checked encoder errors.
pub fn to_datagram_for(&self, version: Version) -> Result<Vec<u8>> {
let changes_layout = version.layout()? != self.version.layout()?;
match &self.message {
Message::ServerInfo(info) => info.to_datagram(),
Message::FrameOfData(frame) => {
if changes_layout
&& [
&frame.skeletons,
&frame.assets,
&frame.force_plates,
&frame.devices,
]
.iter()
.any(|section| section.count != 0 || !section.payload.is_empty())
{
return Err(Error::InvalidValue {
field: "opaque frame sections",
reason: "cross-layout encoding requires explicit reconstruction",
});
}
frame.to_datagram(version)
}
Message::ModelDef(model) => {
if changes_layout
&& model.datasets.iter().any(|dataset| match dataset {
crate::DataSet::RigidBody(body) => !body.tail.is_empty(),
// Bone and asset rigid-body tails carry the source
// layout's marker description.
crate::DataSet::Skeleton(skeleton) => skeleton
.rigid_bodies
.iter()
.any(|body| !body.tail.is_empty()),
crate::DataSet::Asset(asset) => {
asset.rigid_bodies.iter().any(|body| !body.tail.is_empty())
}
crate::DataSet::Other { .. } => true,
// Identical layouts in NatNet 3.0 through 4.1.
crate::DataSet::MarkerSet(_)
| crate::DataSet::ForcePlate(_)
| crate::DataSet::Device(_)
| crate::DataSet::Camera(_) => false,
})
{
return Err(Error::InvalidValue {
field: "opaque MODELDEF data",
reason: "cross-layout encoding requires explicit reconstruction",
});
}
model.to_datagram(version)
}
Message::Other { id, payload } => message::frame_checked(*id, payload),
}
}
}