1use crate::error::{EtherNetIpError, Result};
2use crate::PlcValue;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
7pub struct UdtDefinition {
8 pub name: String,
9 pub members: Vec<UdtMember>,
10}
11
12#[derive(Debug, Clone)]
14pub struct UdtMember {
15 pub name: String,
16 pub data_type: u16,
17 pub offset: u32,
18 pub size: u32,
19}
20
21#[derive(Debug, Clone)]
23pub struct UdtTemplate {
24 pub template_id: u32,
25 pub name: String,
26 pub size: u32,
27 pub member_count: u16,
28 pub members: Vec<UdtMember>,
29}
30
31#[derive(Debug, Clone)]
33pub struct TagAttributes {
34 pub name: String,
35 pub data_type: u16,
36 pub data_type_name: String,
37 pub dimensions: Vec<u32>,
38 pub permissions: TagPermissions,
39 pub scope: TagScope,
40 pub template_instance_id: Option<u32>,
41 pub size: u32,
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub enum TagPermissions {
47 ReadOnly,
48 ReadWrite,
49 WriteOnly,
50 Unknown,
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub enum TagScope {
56 Controller,
57 Program(String),
58 Unknown,
59}
60
61#[derive(Debug)]
63pub struct UdtManager {
64 definitions: HashMap<String, UdtDefinition>,
65 templates: HashMap<u32, UdtTemplate>,
66 tag_attributes: HashMap<String, TagAttributes>,
67}
68
69impl UdtManager {
70 pub fn new() -> Self {
71 Self {
72 definitions: HashMap::new(),
73 templates: HashMap::new(),
74 tag_attributes: HashMap::new(),
75 }
76 }
77
78 pub fn add_definition(&mut self, definition: UdtDefinition) {
80 self.definitions.insert(definition.name.clone(), definition);
81 }
82
83 pub fn get_definition(&self, name: &str) -> Option<&UdtDefinition> {
85 self.definitions.get(name)
86 }
87
88 pub fn add_template(&mut self, template: UdtTemplate) {
90 self.templates.insert(template.template_id, template);
91 }
92
93 pub fn get_template(&self, template_id: u32) -> Option<&UdtTemplate> {
95 self.templates.get(&template_id)
96 }
97
98 pub fn add_tag_attributes(&mut self, attributes: TagAttributes) {
100 self.tag_attributes
101 .insert(attributes.name.clone(), attributes);
102 }
103
104 pub fn get_tag_attributes(&self, name: &str) -> Option<&TagAttributes> {
106 self.tag_attributes.get(name)
107 }
108
109 pub fn list_definitions(&self) -> Vec<String> {
111 self.definitions.keys().cloned().collect()
112 }
113
114 pub fn list_templates(&self) -> Vec<u32> {
116 self.templates.keys().cloned().collect()
117 }
118
119 pub fn list_tag_attributes(&self) -> Vec<String> {
121 self.tag_attributes.keys().cloned().collect()
122 }
123
124 pub fn clear_cache(&mut self) {
126 self.definitions.clear();
127 self.templates.clear();
128 self.tag_attributes.clear();
129 }
130
131 pub fn parse_udt_template(&self, template_id: u32, data: &[u8]) -> Result<UdtTemplate> {
133 if data.len() < 8 {
134 return Err(EtherNetIpError::Protocol(
135 "UDT template data too short".to_string(),
136 ));
137 }
138
139 let mut offset = 0;
140
141 let structure_size = u32::from_le_bytes([
143 data[offset],
144 data[offset + 1],
145 data[offset + 2],
146 data[offset + 3],
147 ]);
148 offset += 4;
149
150 let member_count = u16::from_le_bytes([data[offset], data[offset + 1]]);
151 offset += 2;
152
153 offset += 2;
155
156 let mut members = Vec::new();
157 let mut current_offset = 0u32;
158
159 for i in 0..member_count {
161 if offset + 8 > data.len() {
162 return Err(EtherNetIpError::Protocol(format!(
163 "UDT template member {} data incomplete",
164 i
165 )));
166 }
167
168 let member_info = u32::from_le_bytes([
170 data[offset],
171 data[offset + 1],
172 data[offset + 2],
173 data[offset + 3],
174 ]);
175 offset += 4;
176
177 let member_name_length = u16::from_le_bytes([data[offset], data[offset + 1]]);
178 offset += 2;
179
180 offset += 2;
182
183 let data_type = (member_info & 0xFFFF) as u16;
185 let _dimensions = ((member_info >> 16) & 0xFF) as u8;
186
187 if offset + member_name_length as usize > data.len() {
189 return Err(EtherNetIpError::Protocol(format!(
190 "UDT template member {} name data incomplete",
191 i
192 )));
193 }
194
195 let name_bytes = &data[offset..offset + member_name_length as usize];
196 let member_name = String::from_utf8_lossy(name_bytes).to_string();
197 offset += member_name_length as usize;
198
199 offset = (offset + 3) & !3;
201
202 let member_size = self.get_data_type_size(data_type);
204
205 let member = UdtMember {
207 name: member_name,
208 data_type,
209 offset: current_offset,
210 size: member_size,
211 };
212
213 members.push(member);
214 current_offset += member_size;
215 }
216
217 Ok(UdtTemplate {
218 template_id,
219 name: format!("Template_{}", template_id),
220 size: structure_size,
221 member_count,
222 members,
223 })
224 }
225
226 fn get_data_type_size(&self, data_type: u16) -> u32 {
228 match data_type {
229 0x00C1 => 1, 0x00C2 => 1, 0x00C3 => 2, 0x00C4 => 4, 0x00C5 => 8, 0x00C6 => 1, 0x00C7 => 2, 0x00C8 => 4, 0x00C9 => 8, 0x00CA => 4, 0x00CB => 8, 0x00CE => 88, _ => 4, }
243 }
244
245 pub fn parse_tag_attributes(&self, tag_name: &str, data: &[u8]) -> Result<TagAttributes> {
247 if data.len() < 8 {
248 return Err(EtherNetIpError::Protocol(
249 "Tag attributes data too short".to_string(),
250 ));
251 }
252
253 let mut offset = 0;
254
255 let data_type = u16::from_le_bytes([data[offset], data[offset + 1]]);
257 offset += 2;
258
259 let size = u32::from_le_bytes([
261 data[offset],
262 data[offset + 1],
263 data[offset + 2],
264 data[offset + 3],
265 ]);
266 offset += 4;
267
268 let mut dimensions = Vec::new();
270 if data.len() > offset {
271 let dimension_count = data[offset] as usize;
272 offset += 1;
273
274 for _ in 0..dimension_count {
275 if offset + 4 <= data.len() {
276 let dim = u32::from_le_bytes([
277 data[offset],
278 data[offset + 1],
279 data[offset + 2],
280 data[offset + 3],
281 ]);
282 dimensions.push(dim);
283 offset += 4;
284 }
285 }
286 }
287
288 let permissions = TagPermissions::ReadWrite; let scope = if tag_name.contains(':') {
293 let parts: Vec<&str> = tag_name.split(':').collect();
294 if parts.len() >= 2 {
295 TagScope::Program(parts[0].to_string())
296 } else {
297 TagScope::Controller
298 }
299 } else {
300 TagScope::Controller
301 };
302
303 let data_type_name = self.get_data_type_name(data_type);
305
306 let template_instance_id = if data_type == 0x00A0 {
308 Some(0) } else {
311 None
312 };
313
314 Ok(TagAttributes {
315 name: tag_name.to_string(),
316 data_type,
317 data_type_name,
318 dimensions,
319 permissions,
320 scope,
321 template_instance_id,
322 size,
323 })
324 }
325
326 fn get_data_type_name(&self, data_type: u16) -> String {
328 match data_type {
329 0x00C1 => "BOOL".to_string(),
330 0x00C2 => "SINT".to_string(),
331 0x00C3 => "INT".to_string(),
332 0x00C4 => "DINT".to_string(),
333 0x00C5 => "LINT".to_string(),
334 0x00C6 => "USINT".to_string(),
335 0x00C7 => "UINT".to_string(),
336 0x00C8 => "UDINT".to_string(),
337 0x00C9 => "ULINT".to_string(),
338 0x00CA => "REAL".to_string(),
339 0x00CB => "LREAL".to_string(),
340 0x00CE => "STRING".to_string(),
341 0x00A0 => "UDT".to_string(),
342 _ => format!("UNKNOWN(0x{:04X})", data_type),
343 }
344 }
345
346 pub fn parse_udt_instance(&self, _udt_name: &str, data: &[u8]) -> Result<PlcValue> {
352 Ok(PlcValue::Udt(crate::UdtData {
355 symbol_id: 0, data: data.to_vec(),
357 }))
358 }
359
360 pub fn serialize_udt_instance(
362 &self,
363 _udt_value: &HashMap<String, PlcValue>,
364 ) -> Result<Vec<u8>> {
365 Err(crate::error::EtherNetIpError::Protocol(
366 "UDT instance serialization is not implemented yet".to_string(),
367 ))
368 }
369}
370
371impl Default for UdtManager {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377#[derive(Debug, Clone)]
381pub struct UserDefinedType {
382 pub name: String,
384 pub size: u32,
386 pub members: Vec<UdtMember>,
388 member_offsets: HashMap<String, u32>,
390}
391
392impl UserDefinedType {
393 pub fn new(name: String) -> Self {
395 Self {
396 name,
397 size: 0,
398 members: Vec::new(),
399 member_offsets: HashMap::new(),
400 }
401 }
402
403 pub fn add_member(&mut self, member: UdtMember) {
405 self.member_offsets
406 .insert(member.name.clone(), member.offset);
407 self.members.push(member);
408 self.size = self
410 .members
411 .iter()
412 .map(|m| m.offset + m.size)
413 .max()
414 .unwrap_or(0);
415 }
416
417 pub fn get_member_offset(&self, name: &str) -> Option<u32> {
419 self.member_offsets.get(name).copied()
420 }
421
422 pub fn from_cip_data(_data: &[u8]) -> crate::error::Result<Self> {
424 Err(crate::error::EtherNetIpError::Protocol(
425 "UDT CIP definition parsing is not implemented yet".to_string(),
426 ))
427 }
428
429 pub fn to_hash_map(&self, data: &[u8]) -> crate::error::Result<HashMap<String, PlcValue>> {
431 if data.is_empty() {
432 return Err(crate::error::EtherNetIpError::Protocol(
433 "UDT data is empty".to_string(),
434 ));
435 }
436
437 let mut result = HashMap::new();
438
439 for member in &self.members {
440 let offset = member.offset as usize;
441 if offset + member.size as usize <= data.len() {
442 let member_data = &data[offset..offset + member.size as usize];
443 let value = self.parse_member_value(member, member_data)?;
444 result.insert(member.name.clone(), value);
445 }
446 }
447
448 Ok(result)
449 }
450
451 pub fn from_hash_map(
453 &self,
454 values: &HashMap<String, PlcValue>,
455 ) -> crate::error::Result<Vec<u8>> {
456 let mut data = vec![0u8; self.size as usize];
457
458 for member in &self.members {
459 if let Some(value) = values.get(&member.name) {
460 let member_data = self.serialize_member_value(member, value)?;
461 let offset = member.offset as usize;
462 let end_offset = offset + member_data.len();
463
464 if end_offset <= data.len() {
465 data[offset..end_offset].copy_from_slice(&member_data);
466 } else {
467 return Err(crate::error::EtherNetIpError::Protocol(format!(
468 "Member {} data exceeds UDT size",
469 member.name
470 )));
471 }
472 }
473 }
474
475 Ok(data)
476 }
477
478 pub fn read_member(&self, data: &[u8], member_name: &str) -> crate::error::Result<PlcValue> {
480 if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
481 let offset = member.offset as usize;
482 if offset + member.size as usize <= data.len() {
483 let member_data = &data[offset..offset + member.size as usize];
484 self.parse_member_value(member, member_data)
485 } else {
486 Err(crate::error::EtherNetIpError::Protocol(format!(
487 "Member {} data incomplete",
488 member_name
489 )))
490 }
491 } else {
492 Err(crate::error::EtherNetIpError::TagNotFound(format!(
493 "UDT member '{}' not found",
494 member_name
495 )))
496 }
497 }
498
499 pub fn write_member(
501 &self,
502 data: &mut [u8],
503 member_name: &str,
504 value: &PlcValue,
505 ) -> crate::error::Result<()> {
506 if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
507 let member_data = self.serialize_member_value(member, value)?;
508 let offset = member.offset as usize;
509 let end_offset = offset + member_data.len();
510
511 if end_offset <= data.len() {
512 data[offset..end_offset].copy_from_slice(&member_data);
513 Ok(())
514 } else {
515 Err(crate::error::EtherNetIpError::Protocol(format!(
516 "Member {} data exceeds UDT size",
517 member_name
518 )))
519 }
520 } else {
521 Err(crate::error::EtherNetIpError::TagNotFound(format!(
522 "UDT member '{}' not found",
523 member_name
524 )))
525 }
526 }
527
528 pub fn get_member_size(&self, member_name: &str) -> Option<u32> {
530 self.members
531 .iter()
532 .find(|m| m.name == member_name)
533 .map(|m| m.size)
534 }
535
536 pub fn get_member_data_type(&self, member_name: &str) -> Option<u16> {
538 self.members
539 .iter()
540 .find(|m| m.name == member_name)
541 .map(|m| m.data_type)
542 }
543
544 pub fn parse_member_value(
546 &self,
547 member: &UdtMember,
548 data: &[u8],
549 ) -> crate::error::Result<PlcValue> {
550 match member.data_type {
551 0x00C1 => {
552 if data.is_empty() {
553 return Err(crate::error::EtherNetIpError::Protocol(
554 "BOOL data too short".to_string(),
555 ));
556 }
557 Ok(PlcValue::Bool(data[0] != 0))
558 }
559 0x00C2 => {
560 if data.is_empty() {
562 return Err(crate::error::EtherNetIpError::Protocol(
563 "SINT data too short".to_string(),
564 ));
565 }
566 Ok(PlcValue::Sint(data[0] as i8))
567 }
568 0x00C3 => {
569 if data.len() < 2 {
571 return Err(crate::error::EtherNetIpError::Protocol(
572 "INT data too short".to_string(),
573 ));
574 }
575 let mut bytes = [0u8; 2];
576 bytes.copy_from_slice(&data[..2]);
577 Ok(PlcValue::Int(i16::from_le_bytes(bytes)))
578 }
579 0x00C4 => {
580 if data.len() < 4 {
582 return Err(crate::error::EtherNetIpError::Protocol(
583 "DINT data too short".to_string(),
584 ));
585 }
586 let mut bytes = [0u8; 4];
587 bytes.copy_from_slice(&data[..4]);
588 Ok(PlcValue::Dint(i32::from_le_bytes(bytes)))
589 }
590 0x00C5 => {
591 if data.len() < 8 {
593 return Err(crate::error::EtherNetIpError::Protocol(
594 "LINT data too short".to_string(),
595 ));
596 }
597 let mut bytes = [0u8; 8];
598 bytes.copy_from_slice(&data[..8]);
599 Ok(PlcValue::Lint(i64::from_le_bytes(bytes)))
600 }
601 0x00C6 => {
602 if data.is_empty() {
604 return Err(crate::error::EtherNetIpError::Protocol(
605 "USINT data too short".to_string(),
606 ));
607 }
608 Ok(PlcValue::Usint(data[0]))
609 }
610 0x00C7 => {
611 if data.len() < 2 {
613 return Err(crate::error::EtherNetIpError::Protocol(
614 "UINT data too short".to_string(),
615 ));
616 }
617 let mut bytes = [0u8; 2];
618 bytes.copy_from_slice(&data[..2]);
619 Ok(PlcValue::Uint(u16::from_le_bytes(bytes)))
620 }
621 0x00C8 => {
622 if data.len() < 4 {
624 return Err(crate::error::EtherNetIpError::Protocol(
625 "UDINT data too short".to_string(),
626 ));
627 }
628 let mut bytes = [0u8; 4];
629 bytes.copy_from_slice(&data[..4]);
630 Ok(PlcValue::Udint(u32::from_le_bytes(bytes)))
631 }
632 0x00C9 => {
633 if data.len() < 8 {
635 return Err(crate::error::EtherNetIpError::Protocol(
636 "ULINT data too short".to_string(),
637 ));
638 }
639 let mut bytes = [0u8; 8];
640 bytes.copy_from_slice(&data[..8]);
641 Ok(PlcValue::Ulint(u64::from_le_bytes(bytes)))
642 }
643 0x00CA => {
644 if data.len() < 4 {
646 return Err(crate::error::EtherNetIpError::Protocol(
647 "REAL data too short".to_string(),
648 ));
649 }
650 let mut bytes = [0u8; 4];
651 bytes.copy_from_slice(&data[..4]);
652 Ok(PlcValue::Real(f32::from_le_bytes(bytes)))
653 }
654 0x00CB => {
655 if data.len() < 8 {
657 return Err(crate::error::EtherNetIpError::Protocol(
658 "LREAL data too short".to_string(),
659 ));
660 }
661 let mut bytes = [0u8; 8];
662 bytes.copy_from_slice(&data[..8]);
663 Ok(PlcValue::Lreal(f64::from_le_bytes(bytes)))
664 }
665 0x00CE => {
666 if data.len() < 4 {
668 return Err(crate::error::EtherNetIpError::Protocol(
669 "STRING data too short".to_string(),
670 ));
671 }
672 let length = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
673 if data.len() - 4 < length {
674 return Err(crate::error::EtherNetIpError::Protocol(
675 "STRING data incomplete".to_string(),
676 ));
677 }
678 let string_data = &data[4..4 + length];
679 let string_value = String::from_utf8_lossy(string_data).to_string();
680 Ok(PlcValue::String(string_value))
681 }
682 _ => Err(crate::error::EtherNetIpError::Protocol(format!(
683 "Unsupported UDT data type: 0x{:04X}",
684 member.data_type
685 ))),
686 }
687 }
688
689 pub fn serialize_member_value(
691 &self,
692 member: &UdtMember,
693 value: &PlcValue,
694 ) -> crate::error::Result<Vec<u8>> {
695 match member.data_type {
696 0x00C1 => match value {
697 PlcValue::Bool(b) => Ok(vec![if *b { 0xFF } else { 0x00 }]),
698 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
699 expected: "BOOL".to_string(),
700 actual: format!("{:?}", value),
701 }),
702 },
703 0x00C2 => match value {
704 PlcValue::Sint(s) => Ok(vec![*s as u8]),
705 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
706 expected: "SINT".to_string(),
707 actual: format!("{:?}", value),
708 }),
709 },
710 0x00C3 => match value {
711 PlcValue::Int(i) => Ok(i.to_le_bytes().to_vec()),
712 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
713 expected: "INT".to_string(),
714 actual: format!("{:?}", value),
715 }),
716 },
717 0x00C4 => match value {
718 PlcValue::Dint(d) => Ok(d.to_le_bytes().to_vec()),
719 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
720 expected: "DINT".to_string(),
721 actual: format!("{:?}", value),
722 }),
723 },
724 0x00C5 => match value {
725 PlcValue::Lint(l) => Ok(l.to_le_bytes().to_vec()),
726 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
727 expected: "LINT".to_string(),
728 actual: format!("{:?}", value),
729 }),
730 },
731 0x00C6 => match value {
732 PlcValue::Usint(u) => Ok(vec![*u]),
733 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
734 expected: "USINT".to_string(),
735 actual: format!("{:?}", value),
736 }),
737 },
738 0x00C7 => match value {
739 PlcValue::Uint(u) => Ok(u.to_le_bytes().to_vec()),
740 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
741 expected: "UINT".to_string(),
742 actual: format!("{:?}", value),
743 }),
744 },
745 0x00C8 => match value {
746 PlcValue::Udint(u) => Ok(u.to_le_bytes().to_vec()),
747 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
748 expected: "UDINT".to_string(),
749 actual: format!("{:?}", value),
750 }),
751 },
752 0x00C9 => match value {
753 PlcValue::Ulint(u) => Ok(u.to_le_bytes().to_vec()),
754 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
755 expected: "ULINT".to_string(),
756 actual: format!("{:?}", value),
757 }),
758 },
759 0x00CA => match value {
760 PlcValue::Real(r) => Ok(r.to_le_bytes().to_vec()),
761 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
762 expected: "REAL".to_string(),
763 actual: format!("{:?}", value),
764 }),
765 },
766 0x00CB => match value {
767 PlcValue::Lreal(l) => Ok(l.to_le_bytes().to_vec()),
768 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
769 expected: "LREAL".to_string(),
770 actual: format!("{:?}", value),
771 }),
772 },
773 0x00CE => {
774 match value {
775 PlcValue::String(s) => {
776 let mut result = Vec::new();
777 let max_data_len = member.size.saturating_sub(4); let max_chars = (max_data_len as usize).min(82); let length = (s.len() as u32).min(max_chars as u32);
780 result.extend_from_slice(&length.to_le_bytes());
782 result.extend_from_slice(&s.as_bytes()[..length as usize]);
783 while result.len() < member.size as usize && result.len() % 2 != 0 {
785 result.push(0);
786 }
787 if result.len() > member.size as usize {
789 result.truncate(member.size as usize);
790 }
791 Ok(result)
792 }
793 _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
794 expected: "STRING".to_string(),
795 actual: format!("{:?}", value),
796 }),
797 }
798 }
799 _ => Err(crate::error::EtherNetIpError::Protocol(format!(
800 "Unsupported UDT data type for serialization: 0x{:04X}",
801 member.data_type
802 ))),
803 }
804 }
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810
811 #[test]
812 fn test_udt_member_offsets() {
813 let mut udt = UserDefinedType::new("TestUDT".to_string());
814
815 udt.add_member(UdtMember {
816 name: "Bool1".to_string(),
817 data_type: 0x00C1,
818 offset: 0,
819 size: 1,
820 });
821
822 udt.add_member(UdtMember {
823 name: "Dint1".to_string(),
824 data_type: 0x00C4,
825 offset: 4,
826 size: 4,
827 });
828
829 assert_eq!(udt.get_member_offset("Bool1"), Some(0));
830 assert_eq!(udt.get_member_offset("Dint1"), Some(4));
831 assert_eq!(udt.size, 8);
832 }
833
834 #[test]
835 fn test_udt_parsing() {
836 let mut udt = UserDefinedType::new("TestUDT".to_string());
837
838 udt.add_member(UdtMember {
839 name: "Bool1".to_string(),
840 data_type: 0x00C1,
841 offset: 0,
842 size: 1,
843 });
844
845 udt.add_member(UdtMember {
846 name: "Dint1".to_string(),
847 data_type: 0x00C4,
848 offset: 4,
849 size: 4,
850 });
851
852 let data = vec![0xFF, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00];
853 let result = udt.to_hash_map(&data).unwrap();
854
855 assert_eq!(result.get("Bool1"), Some(&PlcValue::Bool(true)));
856 assert_eq!(result.get("Dint1"), Some(&PlcValue::Dint(42)));
857 }
858
859 #[test]
860 fn test_from_cip_data_returns_explicit_error_until_implemented() {
861 let result = UserDefinedType::from_cip_data(&[0x01, 0x02, 0x03]);
862 assert!(result.is_err());
863 let error_text = result.err().unwrap().to_string();
864 assert!(error_text.contains("not implemented"));
865 }
866
867 #[test]
868 fn test_serialize_udt_instance_returns_explicit_error_until_implemented() {
869 let manager = UdtManager::new();
870 let values = HashMap::new();
871 let result = manager.serialize_udt_instance(&values);
872 assert!(result.is_err());
873 let error_text = result.err().unwrap().to_string();
874 assert!(error_text.contains("not implemented"));
875 }
876}