Skip to main content

mabi_opcua/codec/
data_value.rs

1//! OPC UA DataValue binary encoding/decoding.
2//!
3//! OPC UA Part 6, Section 5.2.2.17 — DataValue encoding:
4//! - Encoding mask byte with flags for which fields are present
5//! - Followed by present fields in order
6
7use bytes::{BufMut, Bytes, BytesMut};
8use chrono::{DateTime, Utc};
9
10use crate::codec::decoder::BinaryDecodable;
11use crate::codec::encoder::BinaryEncodable;
12use crate::error::OpcUaResult;
13use crate::types::{DataValue, StatusCode, Variant};
14
15// DataValue encoding mask bits
16const HAS_VALUE: u8 = 0x01;
17const HAS_STATUS: u8 = 0x02;
18const HAS_SOURCE_TIMESTAMP: u8 = 0x04;
19const HAS_SERVER_TIMESTAMP: u8 = 0x08;
20const HAS_SOURCE_PICOSECONDS: u8 = 0x10;
21const HAS_SERVER_PICOSECONDS: u8 = 0x20;
22
23impl BinaryEncodable for DataValue {
24    fn encode(&self, buf: &mut BytesMut) -> OpcUaResult<()> {
25        let mut mask: u8 = 0;
26        if self.value().is_some() {
27            mask |= HAS_VALUE;
28        }
29        if self.status().raw() != 0 {
30            mask |= HAS_STATUS;
31        }
32        if self.source_timestamp().is_some() {
33            mask |= HAS_SOURCE_TIMESTAMP;
34        }
35        if self.server_timestamp().is_some() {
36            mask |= HAS_SERVER_TIMESTAMP;
37        }
38        if self.source_picoseconds() != 0 {
39            mask |= HAS_SOURCE_PICOSECONDS;
40        }
41        if self.server_picoseconds() != 0 {
42            mask |= HAS_SERVER_PICOSECONDS;
43        }
44
45        buf.put_u8(mask);
46
47        if let Some(v) = self.value() {
48            v.encode(buf)?;
49        }
50        if (mask & HAS_STATUS) != 0 {
51            buf.put_u32_le(self.status().raw());
52        }
53        if let Some(ts) = self.source_timestamp() {
54            ts.encode(buf)?;
55        }
56        if (mask & HAS_SOURCE_PICOSECONDS) != 0 {
57            buf.put_u16_le(self.source_picoseconds());
58        }
59        if let Some(ts) = self.server_timestamp() {
60            ts.encode(buf)?;
61        }
62        if (mask & HAS_SERVER_PICOSECONDS) != 0 {
63            buf.put_u16_le(self.server_picoseconds());
64        }
65
66        Ok(())
67    }
68
69    fn encoded_size(&self) -> usize {
70        let mut size = 1; // mask byte
71        if let Some(v) = self.value() {
72            size += v.encoded_size();
73        }
74        if self.status().raw() != 0 {
75            size += 4;
76        }
77        if self.source_timestamp().is_some() {
78            size += 8;
79        }
80        if self.source_picoseconds() != 0 {
81            size += 2;
82        }
83        if self.server_timestamp().is_some() {
84            size += 8;
85        }
86        if self.server_picoseconds() != 0 {
87            size += 2;
88        }
89        size
90    }
91}
92
93impl BinaryDecodable for DataValue {
94    fn decode(buf: &mut Bytes) -> OpcUaResult<Self> {
95        let mask = u8::decode(buf)?;
96
97        let value = if (mask & HAS_VALUE) != 0 {
98            Some(Variant::decode(buf)?)
99        } else {
100            None
101        };
102
103        let status = if (mask & HAS_STATUS) != 0 {
104            StatusCode::from_raw(u32::decode(buf)?)
105        } else {
106            StatusCode::GOOD
107        };
108
109        let source_timestamp = if (mask & HAS_SOURCE_TIMESTAMP) != 0 {
110            Some(DateTime::<Utc>::decode(buf)?)
111        } else {
112            None
113        };
114
115        let source_picoseconds = if (mask & HAS_SOURCE_PICOSECONDS) != 0 {
116            u16::decode(buf)?
117        } else {
118            0
119        };
120
121        let server_timestamp = if (mask & HAS_SERVER_TIMESTAMP) != 0 {
122            Some(DateTime::<Utc>::decode(buf)?)
123        } else {
124            None
125        };
126
127        let server_picoseconds = if (mask & HAS_SERVER_PICOSECONDS) != 0 {
128            u16::decode(buf)?
129        } else {
130            0
131        };
132
133        let mut dv = if let Some(value) = value {
134            DataValue::with_status(value, status)
135        } else {
136            let mut dv = DataValue::null();
137            dv.set_status(status);
138            dv
139        };
140
141        // Override timestamps if present in the wire data
142        if let Some(ts) = source_timestamp {
143            dv.set_source_timestamp(ts);
144        }
145        if source_picoseconds != 0 {
146            dv.set_source_picoseconds(source_picoseconds);
147        }
148        if let Some(ts) = server_timestamp {
149            dv.set_server_timestamp(ts);
150        }
151        if server_picoseconds != 0 {
152            dv.set_server_picoseconds(server_picoseconds);
153        }
154
155        Ok(dv)
156    }
157}
158
159// =========================================================================
160// QualifiedName and LocalizedText encoding
161// =========================================================================
162
163use crate::nodes::{LocalizedText, QualifiedName};
164
165impl BinaryEncodable for QualifiedName {
166    fn encode(&self, buf: &mut BytesMut) -> OpcUaResult<()> {
167        self.namespace_index.encode(buf)?;
168        self.name.encode(buf)?;
169        Ok(())
170    }
171    fn encoded_size(&self) -> usize {
172        2 + 4 + self.name.len()
173    }
174}
175
176impl BinaryDecodable for QualifiedName {
177    fn decode(buf: &mut Bytes) -> OpcUaResult<Self> {
178        let namespace_index = u16::decode(buf)?;
179        let name = String::decode(buf)?;
180        Ok(QualifiedName::new(namespace_index, name))
181    }
182}
183
184impl BinaryEncodable for LocalizedText {
185    fn encode(&self, buf: &mut BytesMut) -> OpcUaResult<()> {
186        let mut mask: u8 = 0;
187        if !self.locale.is_empty() {
188            mask |= 0x01;
189        }
190        if !self.text.is_empty() {
191            mask |= 0x02;
192        }
193        buf.put_u8(mask);
194        if (mask & 0x01) != 0 {
195            self.locale.encode(buf)?;
196        }
197        if (mask & 0x02) != 0 {
198            self.text.encode(buf)?;
199        }
200        Ok(())
201    }
202    fn encoded_size(&self) -> usize {
203        let mut size = 1; // mask
204        if !self.locale.is_empty() {
205            size += 4 + self.locale.len();
206        }
207        if !self.text.is_empty() {
208            size += 4 + self.text.len();
209        }
210        size
211    }
212}
213
214impl BinaryDecodable for LocalizedText {
215    fn decode(buf: &mut Bytes) -> OpcUaResult<Self> {
216        let mask = u8::decode(buf)?;
217        let locale = if (mask & 0x01) != 0 {
218            String::decode(buf)?
219        } else {
220            String::new()
221        };
222        let text = if (mask & 0x02) != 0 {
223            String::decode(buf)?
224        } else {
225            String::new()
226        };
227        Ok(LocalizedText::new(locale, text))
228    }
229}
230
231// =========================================================================
232// StatusCode encoding (it's a newtype around u32)
233// =========================================================================
234
235impl BinaryEncodable for StatusCode {
236    fn encode(&self, buf: &mut BytesMut) -> OpcUaResult<()> {
237        buf.put_u32_le(self.raw());
238        Ok(())
239    }
240    fn encoded_size(&self) -> usize {
241        4
242    }
243}
244
245impl BinaryDecodable for StatusCode {
246    fn decode(buf: &mut Bytes) -> OpcUaResult<Self> {
247        Ok(StatusCode::from_raw(u32::decode(buf)?))
248    }
249}
250
251// =========================================================================
252// ExtensionObject encoding — used for service request/response wrapping
253// =========================================================================
254
255/// OPC UA ExtensionObject — wraps a service request or response body.
256///
257/// Encoding: NodeId (type_id) + encoding byte + body
258/// - encoding 0x00: no body
259/// - encoding 0x01: ByteString body (binary encoded)
260/// - encoding 0x02: XmlElement body
261#[derive(Debug, Clone)]
262pub struct ExtensionObject {
263    /// The NodeId of the type being wrapped.
264    pub type_id: crate::types::NodeId,
265    /// The raw encoded body (binary).
266    pub body: Option<Vec<u8>>,
267}
268
269impl BinaryEncodable for ExtensionObject {
270    fn encode(&self, buf: &mut BytesMut) -> OpcUaResult<()> {
271        self.type_id.encode(buf)?;
272        match &self.body {
273            Some(body) => {
274                buf.put_u8(0x01); // Binary body
275                buf.put_i32_le(body.len() as i32);
276                buf.put_slice(body);
277            }
278            None => {
279                buf.put_u8(0x00); // No body
280            }
281        }
282        Ok(())
283    }
284    fn encoded_size(&self) -> usize {
285        self.type_id.encoded_size()
286            + 1
287            + match &self.body {
288                Some(body) => 4 + body.len(),
289                None => 0,
290            }
291    }
292}
293
294impl BinaryDecodable for ExtensionObject {
295    fn decode(buf: &mut Bytes) -> OpcUaResult<Self> {
296        let type_id = crate::types::NodeId::decode(buf)?;
297        let encoding = u8::decode(buf)?;
298        let body = match encoding {
299            0x00 => None,
300            0x01 => {
301                let body_bytes = Vec::<u8>::decode(buf)?;
302                Some(body_bytes)
303            }
304            0x02 => {
305                // XML body — decode as string, store as bytes
306                let xml = String::decode(buf)?;
307                Some(xml.into_bytes())
308            }
309            _ => {
310                return Err(crate::error::OpcUaError::Codec(format!(
311                    "Unknown ExtensionObject encoding: 0x{:02X}",
312                    encoding
313                )));
314            }
315        };
316        Ok(ExtensionObject { type_id, body })
317    }
318}
319
320// =========================================================================
321// DiagnosticInfo encoding
322// =========================================================================
323
324/// OPC UA DiagnosticInfo.
325#[derive(Debug, Clone, Default)]
326pub struct DiagnosticInfo {
327    pub symbolic_id: Option<i32>,
328    pub namespace_uri: Option<i32>,
329    pub locale: Option<i32>,
330    pub localized_text: Option<i32>,
331    pub additional_info: Option<String>,
332    pub inner_status_code: Option<StatusCode>,
333    pub inner_diagnostic_info: Option<Box<DiagnosticInfo>>,
334}
335
336impl BinaryEncodable for DiagnosticInfo {
337    fn encode(&self, buf: &mut BytesMut) -> OpcUaResult<()> {
338        let mut mask: u8 = 0;
339        if self.symbolic_id.is_some() {
340            mask |= 0x01;
341        }
342        if self.namespace_uri.is_some() {
343            mask |= 0x02;
344        }
345        if self.localized_text.is_some() {
346            mask |= 0x04;
347        }
348        if self.locale.is_some() {
349            mask |= 0x08;
350        }
351        if self.additional_info.is_some() {
352            mask |= 0x10;
353        }
354        if self.inner_status_code.is_some() {
355            mask |= 0x20;
356        }
357        if self.inner_diagnostic_info.is_some() {
358            mask |= 0x40;
359        }
360
361        buf.put_u8(mask);
362        if let Some(v) = self.symbolic_id {
363            v.encode(buf)?;
364        }
365        if let Some(v) = self.namespace_uri {
366            v.encode(buf)?;
367        }
368        if let Some(v) = self.localized_text {
369            v.encode(buf)?;
370        }
371        if let Some(v) = self.locale {
372            v.encode(buf)?;
373        }
374        if let Some(v) = &self.additional_info {
375            v.encode(buf)?;
376        }
377        if let Some(v) = &self.inner_status_code {
378            v.encode(buf)?;
379        }
380        if let Some(v) = &self.inner_diagnostic_info {
381            v.encode(buf)?;
382        }
383        Ok(())
384    }
385    fn encoded_size(&self) -> usize {
386        let mut size = 1;
387        if self.symbolic_id.is_some() {
388            size += 4;
389        }
390        if self.namespace_uri.is_some() {
391            size += 4;
392        }
393        if self.localized_text.is_some() {
394            size += 4;
395        }
396        if self.locale.is_some() {
397            size += 4;
398        }
399        if let Some(s) = &self.additional_info {
400            size += 4 + s.len();
401        }
402        if self.inner_status_code.is_some() {
403            size += 4;
404        }
405        if let Some(d) = &self.inner_diagnostic_info {
406            size += d.encoded_size();
407        }
408        size
409    }
410}
411
412impl BinaryDecodable for DiagnosticInfo {
413    fn decode(buf: &mut Bytes) -> OpcUaResult<Self> {
414        let mask = u8::decode(buf)?;
415        Ok(DiagnosticInfo {
416            symbolic_id: if (mask & 0x01) != 0 {
417                Some(i32::decode(buf)?)
418            } else {
419                None
420            },
421            namespace_uri: if (mask & 0x02) != 0 {
422                Some(i32::decode(buf)?)
423            } else {
424                None
425            },
426            localized_text: if (mask & 0x04) != 0 {
427                Some(i32::decode(buf)?)
428            } else {
429                None
430            },
431            locale: if (mask & 0x08) != 0 {
432                Some(i32::decode(buf)?)
433            } else {
434                None
435            },
436            additional_info: if (mask & 0x10) != 0 {
437                Some(String::decode(buf)?)
438            } else {
439                None
440            },
441            inner_status_code: if (mask & 0x20) != 0 {
442                Some(StatusCode::decode(buf)?)
443            } else {
444                None
445            },
446            inner_diagnostic_info: if (mask & 0x40) != 0 {
447                Some(Box::new(DiagnosticInfo::decode(buf)?))
448            } else {
449                None
450            },
451        })
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    fn roundtrip_dv(dv: &DataValue) -> DataValue {
460        let mut buf = BytesMut::new();
461        dv.encode(&mut buf).unwrap();
462        let mut data = buf.freeze();
463        DataValue::decode(&mut data).unwrap()
464    }
465
466    #[test]
467    fn test_null_datavalue() {
468        let dv = DataValue::null();
469        let result = roundtrip_dv(&dv);
470        assert!(result.value().is_none());
471    }
472
473    #[test]
474    fn test_datavalue_with_value() {
475        let dv = DataValue::new(Variant::Double(42.0));
476        let result = roundtrip_dv(&dv);
477        assert_eq!(result.value(), Some(&Variant::Double(42.0)));
478    }
479
480    #[test]
481    fn test_extension_object_roundtrip() {
482        let eo = ExtensionObject {
483            type_id: crate::types::NodeId::numeric(0, 461),
484            body: Some(vec![1, 2, 3, 4]),
485        };
486        let mut buf = BytesMut::new();
487        eo.encode(&mut buf).unwrap();
488        let mut data = buf.freeze();
489        let result = ExtensionObject::decode(&mut data).unwrap();
490        assert_eq!(result.type_id, eo.type_id);
491        assert_eq!(result.body, eo.body);
492    }
493
494    #[test]
495    fn test_qualified_name_roundtrip() {
496        let qn = QualifiedName::new(2, "Temperature");
497        let mut buf = BytesMut::new();
498        qn.encode(&mut buf).unwrap();
499        let mut data = buf.freeze();
500        let result = QualifiedName::decode(&mut data).unwrap();
501        assert_eq!(result.namespace_index, 2);
502        assert_eq!(result.name, "Temperature");
503    }
504
505    #[test]
506    fn test_localized_text_roundtrip() {
507        let lt = LocalizedText::new("en", "Hello");
508        let mut buf = BytesMut::new();
509        lt.encode(&mut buf).unwrap();
510        let mut data = buf.freeze();
511        let result = LocalizedText::decode(&mut data).unwrap();
512        assert_eq!(result.locale, "en");
513        assert_eq!(result.text, "Hello");
514    }
515
516    #[test]
517    fn test_diagnostic_info_roundtrip() {
518        let di = DiagnosticInfo {
519            symbolic_id: Some(1),
520            additional_info: Some("test".into()),
521            ..Default::default()
522        };
523        let mut buf = BytesMut::new();
524        di.encode(&mut buf).unwrap();
525        let mut data = buf.freeze();
526        let result = DiagnosticInfo::decode(&mut data).unwrap();
527        assert_eq!(result.symbolic_id, Some(1));
528        assert_eq!(result.additional_info.as_deref(), Some("test"));
529    }
530}