1use rust_ethernet_ip_types::{PlcValue, TypeError, UdtCodec, UdtData};
2
3pub type Result<T> = std::result::Result<T, UdtError>;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum UdtError {
7 Protocol(String),
8 TagNotFound(String),
9 DataTypeMismatch { expected: String, actual: String },
10}
11
12impl UdtError {
13 pub fn protocol(message: impl Into<String>) -> Self {
14 Self::Protocol(message.into())
15 }
16}
17
18impl std::fmt::Display for UdtError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 UdtError::Protocol(message) => write!(f, "Protocol error: {message}"),
22 UdtError::TagNotFound(tag) => write!(f, "Tag not found: {tag}"),
23 UdtError::DataTypeMismatch { expected, actual } => {
24 write!(f, "Data type mismatch: expected {expected}, got {actual}")
25 }
26 }
27 }
28}
29
30impl std::error::Error for UdtError {}
31use std::collections::HashMap;
32
33#[derive(Debug, Clone)]
35pub struct UdtDefinition {
36 pub name: String,
37 pub members: Vec<UdtMember>,
38}
39
40#[derive(Debug, Clone)]
42pub struct UdtMember {
43 pub name: String,
44 pub data_type: u16,
45 pub offset: u32,
46 pub size: u32,
47}
48
49#[derive(Debug, Clone)]
51pub struct UdtTemplate {
52 pub template_id: u32,
53 pub name: String,
54 pub size: u32,
55 pub member_count: u16,
56 pub members: Vec<UdtMember>,
57}
58
59#[derive(Debug, Clone)]
61pub struct TagAttributes {
62 pub name: String,
63 pub data_type: u16,
64 pub data_type_name: String,
65 pub dimensions: Vec<u32>,
66 pub permissions: TagPermissions,
67 pub scope: TagScope,
68 pub template_instance_id: Option<u32>,
69 pub size: u32,
70}
71
72#[derive(Clone, Copy)]
73struct RawMember {
74 info: u16,
75 raw_type: u16,
76 offset: u32,
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub enum TagPermissions {
82 ReadOnly,
83 ReadWrite,
84 WriteOnly,
85 Unknown,
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub enum TagScope {
91 Controller,
92 Program(String),
93 Unknown,
94}
95
96#[derive(Debug)]
98pub struct UdtManager {
99 definitions: HashMap<String, UdtDefinition>,
100 templates: HashMap<u32, UdtTemplate>,
101 tag_attributes: HashMap<String, TagAttributes>,
102}
103
104impl UdtManager {
105 pub fn new() -> Self {
106 Self {
107 definitions: HashMap::new(),
108 templates: HashMap::new(),
109 tag_attributes: HashMap::new(),
110 }
111 }
112
113 pub fn add_definition(&mut self, definition: UdtDefinition) {
115 self.definitions.insert(definition.name.clone(), definition);
116 }
117
118 pub fn get_definition(&self, name: &str) -> Option<&UdtDefinition> {
120 self.definitions.get(name)
121 }
122
123 pub fn add_template(&mut self, template: UdtTemplate) {
125 self.templates.insert(template.template_id, template);
126 }
127
128 pub fn get_template(&self, template_id: u32) -> Option<&UdtTemplate> {
130 self.templates.get(&template_id)
131 }
132
133 pub fn add_tag_attributes(&mut self, attributes: TagAttributes) {
135 self.tag_attributes
136 .insert(attributes.name.clone(), attributes);
137 }
138
139 pub fn get_tag_attributes(&self, name: &str) -> Option<&TagAttributes> {
141 self.tag_attributes.get(name)
142 }
143
144 pub fn list_definitions(&self) -> Vec<String> {
146 self.definitions.keys().cloned().collect()
147 }
148
149 pub fn list_templates(&self) -> Vec<u32> {
151 self.templates.keys().cloned().collect()
152 }
153
154 pub fn list_tag_attributes(&self) -> Vec<String> {
156 self.tag_attributes.keys().cloned().collect()
157 }
158
159 pub fn clear_cache(&mut self) {
161 self.definitions.clear();
162 self.templates.clear();
163 self.tag_attributes.clear();
164 }
165
166 pub fn parse_udt_template(
168 &self,
169 template_id: u32,
170 member_count: u16,
171 structure_size: u32,
172 data: &[u8],
173 ) -> Result<UdtTemplate> {
174 let member_section_len = member_count as usize * 8;
175 if data.len() < member_section_len {
176 return Err(UdtError::protocol(
177 "UDT template data too short".to_string(),
178 ));
179 }
180
181 let mut raw_members = Vec::with_capacity(member_count as usize);
182 for i in 0..member_count as usize {
183 let base = i * 8;
184 raw_members.push(RawMember {
185 info: u16::from_le_bytes([data[base], data[base + 1]]),
186 raw_type: u16::from_le_bytes([data[base + 2], data[base + 3]]),
187 offset: u32::from_le_bytes([
188 data[base + 4],
189 data[base + 5],
190 data[base + 6],
191 data[base + 7],
192 ]),
193 });
194 }
195
196 let strings = self.parse_null_terminated_strings(&data[member_section_len..]);
197 let template_name = strings
198 .first()
199 .and_then(|value| value.split(';').next())
200 .filter(|value| !value.is_empty())
201 .map(|value| value.to_string())
202 .unwrap_or_else(|| format!("Template_{}", template_id));
203
204 let member_names = strings.into_iter().skip(1);
205 let mut members = Vec::new();
206
207 for (raw_member, member_name) in raw_members.into_iter().zip(member_names) {
208 if member_name.is_empty() || member_name.starts_with("ZZZZZZZZZZ") {
209 continue;
210 }
211
212 let normalized_type = self.normalize_member_data_type(raw_member.raw_type);
213 let member_size = self.estimate_member_size(
214 raw_member,
215 normalized_type,
216 &data[..member_section_len],
217 structure_size,
218 );
219
220 members.push(UdtMember {
221 name: member_name,
222 data_type: normalized_type,
223 offset: raw_member.offset,
224 size: member_size,
225 });
226 }
227
228 Ok(UdtTemplate {
229 template_id,
230 name: template_name,
231 size: structure_size,
232 member_count,
233 members,
234 })
235 }
236
237 fn parse_null_terminated_strings(&self, data: &[u8]) -> Vec<String> {
238 data.split(|byte| *byte == 0)
239 .map(|chunk| String::from_utf8_lossy(chunk).to_string())
240 .collect()
241 }
242
243 fn normalize_member_data_type(&self, raw_type: u16) -> u16 {
244 let base_type = raw_type & 0x0FFF;
245 let is_structure = (raw_type & 0x8000) != 0;
246
247 if is_structure {
248 0x00A0
249 } else if base_type != 0 {
250 base_type
251 } else {
252 raw_type
253 }
254 }
255
256 fn estimate_member_size(
257 &self,
258 raw_member: RawMember,
259 normalized_type: u16,
260 member_section: &[u8],
261 structure_size: u32,
262 ) -> u32 {
263 let is_array = (raw_member.raw_type & 0x2000) != 0;
264 let is_structure = (raw_member.raw_type & 0x8000) != 0;
265
266 if normalized_type == 0x00C1 {
267 return 1;
268 }
269
270 if is_array {
271 let element_size = self.get_data_type_size(normalized_type);
272 return if normalized_type == 0x00D3 {
273 element_size
274 } else {
275 element_size.saturating_mul(raw_member.info as u32)
276 };
277 }
278
279 if is_structure {
280 let next_offset = member_section
281 .as_chunks::<8>()
282 .0
283 .iter()
284 .filter_map(|chunk| {
285 let offset = u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]);
286 (offset > raw_member.offset).then_some(offset)
287 })
288 .min()
289 .unwrap_or(structure_size);
290
291 return next_offset.saturating_sub(raw_member.offset).max(1);
292 }
293
294 self.get_data_type_size(normalized_type)
295 }
296
297 fn get_data_type_size(&self, data_type: u16) -> u32 {
299 match data_type {
300 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, }
314 }
315
316 pub fn parse_tag_attributes(&self, tag_name: &str, data: &[u8]) -> Result<TagAttributes> {
318 if data.len() < 8 {
319 return Err(UdtError::protocol(
320 "Tag attributes data too short".to_string(),
321 ));
322 }
323
324 let mut offset = 0;
325
326 let data_type = u16::from_le_bytes([data[offset], data[offset + 1]]);
328 offset += 2;
329
330 let size = u32::from_le_bytes([
332 data[offset],
333 data[offset + 1],
334 data[offset + 2],
335 data[offset + 3],
336 ]);
337 offset += 4;
338
339 let mut dimensions = Vec::new();
341 if data.len() > offset {
342 let dimension_count = data[offset] as usize;
343 offset += 1;
344
345 for _ in 0..dimension_count {
346 if offset + 4 <= data.len() {
347 let dim = u32::from_le_bytes([
348 data[offset],
349 data[offset + 1],
350 data[offset + 2],
351 data[offset + 3],
352 ]);
353 dimensions.push(dim);
354 offset += 4;
355 }
356 }
357 }
358
359 let permissions = TagPermissions::ReadWrite; let scope = if tag_name.contains(':') {
364 let parts: Vec<&str> = tag_name.split(':').collect();
365 if parts.len() >= 2 {
366 TagScope::Program(parts[0].to_string())
367 } else {
368 TagScope::Controller
369 }
370 } else {
371 TagScope::Controller
372 };
373
374 let data_type_name = self.get_data_type_name(data_type);
376
377 let template_instance_id = if data_type == 0x00A0 {
379 Some(0) } else {
382 None
383 };
384
385 Ok(TagAttributes {
386 name: tag_name.to_string(),
387 data_type,
388 data_type_name,
389 dimensions,
390 permissions,
391 scope,
392 template_instance_id,
393 size,
394 })
395 }
396
397 fn get_data_type_name(&self, data_type: u16) -> String {
399 match data_type {
400 0x00C1 => "BOOL".to_string(),
401 0x00C2 => "SINT".to_string(),
402 0x00C3 => "INT".to_string(),
403 0x00C4 => "DINT".to_string(),
404 0x00C5 => "LINT".to_string(),
405 0x00C6 => "USINT".to_string(),
406 0x00C7 => "UINT".to_string(),
407 0x00C8 => "UDINT".to_string(),
408 0x00C9 => "ULINT".to_string(),
409 0x00CA => "REAL".to_string(),
410 0x00CB => "LREAL".to_string(),
411 0x00CE => "STRING".to_string(),
412 0x00A0 => "UDT".to_string(),
413 _ => format!("UNKNOWN(0x{:04X})", data_type),
414 }
415 }
416
417 pub fn parse_udt_instance(&self, _udt_name: &str, data: &[u8]) -> Result<PlcValue> {
423 Ok(PlcValue::Udt(UdtData {
426 symbol_id: 0, data: data.to_vec(),
428 }))
429 }
430
431 pub fn serialize_udt_instance(
433 &self,
434 _udt_value: &HashMap<String, PlcValue>,
435 ) -> Result<Vec<u8>> {
436 Err(UdtError::protocol(
437 "UDT instance serialization is not implemented yet".to_string(),
438 ))
439 }
440}
441
442impl Default for UdtManager {
443 fn default() -> Self {
444 Self::new()
445 }
446}
447
448#[derive(Debug, Clone)]
452pub struct UserDefinedType {
453 pub name: String,
455 pub size: u32,
457 pub members: Vec<UdtMember>,
459 member_offsets: HashMap<String, u32>,
461 member_bit_indices: HashMap<String, u8>,
463}
464
465impl UserDefinedType {
466 pub fn new(name: String) -> Self {
468 Self {
469 name,
470 size: 0,
471 members: Vec::new(),
472 member_offsets: HashMap::new(),
473 member_bit_indices: HashMap::new(),
474 }
475 }
476
477 pub fn add_member(&mut self, member: UdtMember) {
479 self.add_member_with_bit_index(member, None);
480 }
481
482 pub fn add_member_with_bit_index(&mut self, member: UdtMember, bit_index: Option<u8>) {
484 self.member_offsets
485 .insert(member.name.clone(), member.offset);
486 if let Some(bit_index) = bit_index {
487 self.member_bit_indices
488 .insert(member.name.clone(), bit_index.min(7));
489 }
490 self.members.push(member);
491 self.size = self
493 .members
494 .iter()
495 .map(|m| m.offset + m.size)
496 .max()
497 .unwrap_or(0);
498 }
499
500 pub fn get_member_offset(&self, name: &str) -> Option<u32> {
502 self.member_offsets.get(name).copied()
503 }
504
505 pub fn from_cip_data(_data: &[u8]) -> Result<Self> {
507 Err(UdtError::protocol(
508 "UDT CIP definition parsing is not implemented yet".to_string(),
509 ))
510 }
511
512 pub fn to_hash_map(&self, data: &[u8]) -> Result<HashMap<String, PlcValue>> {
514 if data.is_empty() {
515 return Err(UdtError::protocol("UDT data is empty".to_string()));
516 }
517
518 let mut result = HashMap::new();
519
520 for member in &self.members {
521 let offset = member.offset as usize;
522 let size = member.size as usize;
523 if offset.checked_add(size).is_none_or(|end| end > data.len()) {
524 return Err(UdtError::protocol(format!(
525 "Member {} data incomplete: offset {} size {} exceeds UDT data length {}",
526 member.name,
527 member.offset,
528 member.size,
529 data.len()
530 )));
531 }
532
533 let member_data = &data[offset..offset + size];
534 let value = self.parse_member_value(member, member_data)?;
535 result.insert(member.name.clone(), value);
536 }
537
538 Ok(result)
539 }
540
541 pub fn from_hash_map(&self, values: &HashMap<String, PlcValue>) -> Result<Vec<u8>> {
543 let mut data = vec![0u8; self.size as usize];
544
545 for member in &self.members {
546 let Some(value) = values.get(&member.name) else {
547 return Err(UdtError::protocol(format!(
548 "Missing UDT member value: {}",
549 member.name
550 )));
551 };
552
553 if member.data_type == 0x00C1
554 && let Some(bit_index) = self.member_bit_indices.get(&member.name)
555 {
556 let PlcValue::Bool(value) = value else {
557 return Err(UdtError::DataTypeMismatch {
558 expected: "BOOL".to_string(),
559 actual: format!("{:?}", value),
560 });
561 };
562 let offset = member.offset as usize;
563 let Some(byte) = data.get_mut(offset) else {
564 return Err(UdtError::protocol(format!(
565 "Member {} data exceeds UDT size",
566 member.name
567 )));
568 };
569 let mask = 1_u8 << (*bit_index).min(7);
570 if *value {
571 *byte |= mask;
572 } else {
573 *byte &= !mask;
574 }
575 continue;
576 }
577
578 let member_data = self.serialize_member_value(member, value)?;
579 let offset = member.offset as usize;
580 let end_offset = offset + member_data.len();
581
582 if end_offset <= data.len() {
583 data[offset..end_offset].copy_from_slice(&member_data);
584 } else {
585 return Err(UdtError::protocol(format!(
586 "Member {} data exceeds UDT size",
587 member.name
588 )));
589 }
590 }
591
592 Ok(data)
593 }
594
595 pub fn read_member(&self, data: &[u8], member_name: &str) -> Result<PlcValue> {
597 if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
598 let offset = member.offset as usize;
599 if offset + member.size as usize <= data.len() {
600 let member_data = &data[offset..offset + member.size as usize];
601 self.parse_member_value(member, member_data)
602 } else {
603 Err(UdtError::protocol(format!(
604 "Member {} data incomplete",
605 member_name
606 )))
607 }
608 } else {
609 Err(UdtError::TagNotFound(format!(
610 "UDT member '{}' not found",
611 member_name
612 )))
613 }
614 }
615
616 pub fn write_member(&self, data: &mut [u8], member_name: &str, value: &PlcValue) -> Result<()> {
618 if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
619 let member_data = self.serialize_member_value(member, value)?;
620 let offset = member.offset as usize;
621 let end_offset = offset + member_data.len();
622
623 if end_offset <= data.len() {
624 data[offset..end_offset].copy_from_slice(&member_data);
625 Ok(())
626 } else {
627 Err(UdtError::protocol(format!(
628 "Member {} data exceeds UDT size",
629 member_name
630 )))
631 }
632 } else {
633 Err(UdtError::TagNotFound(format!(
634 "UDT member '{}' not found",
635 member_name
636 )))
637 }
638 }
639
640 pub fn get_member_size(&self, member_name: &str) -> Option<u32> {
642 self.members
643 .iter()
644 .find(|m| m.name == member_name)
645 .map(|m| m.size)
646 }
647
648 pub fn get_member_data_type(&self, member_name: &str) -> Option<u16> {
650 self.members
651 .iter()
652 .find(|m| m.name == member_name)
653 .map(|m| m.data_type)
654 }
655
656 pub fn parse_member_value(&self, member: &UdtMember, data: &[u8]) -> Result<PlcValue> {
658 match member.data_type {
659 0x00C1 => {
660 if data.is_empty() {
661 return Err(UdtError::protocol("BOOL data too short".to_string()));
662 }
663 let value = if let Some(bit_index) = self.member_bit_indices.get(&member.name) {
664 let mask = 1_u8 << (*bit_index).min(7);
665 data[0] & mask != 0
666 } else {
667 data[0] != 0
668 };
669 Ok(PlcValue::Bool(value))
670 }
671 0x00C2 => {
672 if data.is_empty() {
674 return Err(UdtError::protocol("SINT data too short".to_string()));
675 }
676 Ok(PlcValue::Sint(data[0] as i8))
677 }
678 0x00C3 => {
679 if data.len() < 2 {
681 return Err(UdtError::protocol("INT data too short".to_string()));
682 }
683 let mut bytes = [0u8; 2];
684 bytes.copy_from_slice(&data[..2]);
685 Ok(PlcValue::Int(i16::from_le_bytes(bytes)))
686 }
687 0x00C4 => {
688 if data.len() < 4 {
690 return Err(UdtError::protocol("DINT data too short".to_string()));
691 }
692 let mut bytes = [0u8; 4];
693 bytes.copy_from_slice(&data[..4]);
694 Ok(PlcValue::Dint(i32::from_le_bytes(bytes)))
695 }
696 0x00C5 => {
697 if data.len() < 8 {
699 return Err(UdtError::protocol("LINT data too short".to_string()));
700 }
701 let mut bytes = [0u8; 8];
702 bytes.copy_from_slice(&data[..8]);
703 Ok(PlcValue::Lint(i64::from_le_bytes(bytes)))
704 }
705 0x00C6 => {
706 if data.is_empty() {
708 return Err(UdtError::protocol("USINT data too short".to_string()));
709 }
710 Ok(PlcValue::Usint(data[0]))
711 }
712 0x00C7 => {
713 if data.len() < 2 {
715 return Err(UdtError::protocol("UINT data too short".to_string()));
716 }
717 let mut bytes = [0u8; 2];
718 bytes.copy_from_slice(&data[..2]);
719 Ok(PlcValue::Uint(u16::from_le_bytes(bytes)))
720 }
721 0x00C8 => {
722 if data.len() < 4 {
724 return Err(UdtError::protocol("UDINT data too short".to_string()));
725 }
726 let mut bytes = [0u8; 4];
727 bytes.copy_from_slice(&data[..4]);
728 Ok(PlcValue::Udint(u32::from_le_bytes(bytes)))
729 }
730 0x00C9 => {
731 if data.len() < 8 {
733 return Err(UdtError::protocol("ULINT data too short".to_string()));
734 }
735 let mut bytes = [0u8; 8];
736 bytes.copy_from_slice(&data[..8]);
737 Ok(PlcValue::Ulint(u64::from_le_bytes(bytes)))
738 }
739 0x00CA => {
740 if data.len() < 4 {
742 return Err(UdtError::protocol("REAL data too short".to_string()));
743 }
744 let mut bytes = [0u8; 4];
745 bytes.copy_from_slice(&data[..4]);
746 Ok(PlcValue::Real(f32::from_le_bytes(bytes)))
747 }
748 0x00CB => {
749 if data.len() < 8 {
751 return Err(UdtError::protocol("LREAL data too short".to_string()));
752 }
753 let mut bytes = [0u8; 8];
754 bytes.copy_from_slice(&data[..8]);
755 Ok(PlcValue::Lreal(f64::from_le_bytes(bytes)))
756 }
757 0x00CE => {
758 if data.len() < 4 {
760 return Err(UdtError::protocol("STRING data too short".to_string()));
761 }
762 let length = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
763 if data.len() - 4 < length {
764 return Err(UdtError::protocol("STRING data incomplete".to_string()));
765 }
766 let string_data = &data[4..4 + length];
767 let string_value = String::from_utf8_lossy(string_data).to_string();
768 Ok(PlcValue::String(string_value))
769 }
770 _ => Err(UdtError::protocol(format!(
771 "Unsupported UDT data type: 0x{:04X}",
772 member.data_type
773 ))),
774 }
775 }
776
777 pub fn serialize_member_value(&self, member: &UdtMember, value: &PlcValue) -> Result<Vec<u8>> {
779 match member.data_type {
780 0x00C1 => match value {
781 PlcValue::Bool(b) => Ok(vec![if *b { 0xFF } else { 0x00 }]),
782 _ => Err(UdtError::DataTypeMismatch {
783 expected: "BOOL".to_string(),
784 actual: format!("{:?}", value),
785 }),
786 },
787 0x00C2 => match value {
788 PlcValue::Sint(s) => Ok(vec![*s as u8]),
789 _ => Err(UdtError::DataTypeMismatch {
790 expected: "SINT".to_string(),
791 actual: format!("{:?}", value),
792 }),
793 },
794 0x00C3 => match value {
795 PlcValue::Int(i) => Ok(i.to_le_bytes().to_vec()),
796 _ => Err(UdtError::DataTypeMismatch {
797 expected: "INT".to_string(),
798 actual: format!("{:?}", value),
799 }),
800 },
801 0x00C4 => match value {
802 PlcValue::Dint(d) => Ok(d.to_le_bytes().to_vec()),
803 _ => Err(UdtError::DataTypeMismatch {
804 expected: "DINT".to_string(),
805 actual: format!("{:?}", value),
806 }),
807 },
808 0x00C5 => match value {
809 PlcValue::Lint(l) => Ok(l.to_le_bytes().to_vec()),
810 _ => Err(UdtError::DataTypeMismatch {
811 expected: "LINT".to_string(),
812 actual: format!("{:?}", value),
813 }),
814 },
815 0x00C6 => match value {
816 PlcValue::Usint(u) => Ok(vec![*u]),
817 _ => Err(UdtError::DataTypeMismatch {
818 expected: "USINT".to_string(),
819 actual: format!("{:?}", value),
820 }),
821 },
822 0x00C7 => match value {
823 PlcValue::Uint(u) => Ok(u.to_le_bytes().to_vec()),
824 _ => Err(UdtError::DataTypeMismatch {
825 expected: "UINT".to_string(),
826 actual: format!("{:?}", value),
827 }),
828 },
829 0x00C8 => match value {
830 PlcValue::Udint(u) => Ok(u.to_le_bytes().to_vec()),
831 _ => Err(UdtError::DataTypeMismatch {
832 expected: "UDINT".to_string(),
833 actual: format!("{:?}", value),
834 }),
835 },
836 0x00C9 => match value {
837 PlcValue::Ulint(u) => Ok(u.to_le_bytes().to_vec()),
838 _ => Err(UdtError::DataTypeMismatch {
839 expected: "ULINT".to_string(),
840 actual: format!("{:?}", value),
841 }),
842 },
843 0x00CA => match value {
844 PlcValue::Real(r) => Ok(r.to_le_bytes().to_vec()),
845 _ => Err(UdtError::DataTypeMismatch {
846 expected: "REAL".to_string(),
847 actual: format!("{:?}", value),
848 }),
849 },
850 0x00CB => match value {
851 PlcValue::Lreal(l) => Ok(l.to_le_bytes().to_vec()),
852 _ => Err(UdtError::DataTypeMismatch {
853 expected: "LREAL".to_string(),
854 actual: format!("{:?}", value),
855 }),
856 },
857 0x00CE => {
858 match value {
859 PlcValue::String(s) => {
860 let mut result = Vec::new();
861 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);
864 result.extend_from_slice(&length.to_le_bytes());
866 result.extend_from_slice(&s.as_bytes()[..length as usize]);
867 while result.len() < member.size as usize && result.len() % 2 != 0 {
869 result.push(0);
870 }
871 if result.len() > member.size as usize {
873 result.truncate(member.size as usize);
874 }
875 Ok(result)
876 }
877 _ => Err(UdtError::DataTypeMismatch {
878 expected: "STRING".to_string(),
879 actual: format!("{:?}", value),
880 }),
881 }
882 }
883 _ => Err(UdtError::protocol(format!(
884 "Unsupported UDT data type for serialization: 0x{:04X}",
885 member.data_type
886 ))),
887 }
888 }
889}
890
891impl UdtCodec for UserDefinedType {
892 fn to_hash_map(
893 &self,
894 data: &[u8],
895 ) -> rust_ethernet_ip_types::Result<HashMap<String, PlcValue>> {
896 UserDefinedType::to_hash_map(self, data).map_err(|error| TypeError::new(error.to_string()))
897 }
898
899 fn encode_hash_map(
900 &self,
901 values: &HashMap<String, PlcValue>,
902 ) -> rust_ethernet_ip_types::Result<Vec<u8>> {
903 UserDefinedType::from_hash_map(self, values)
904 .map_err(|error| TypeError::new(error.to_string()))
905 }
906}
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911
912 #[test]
913 fn test_udt_member_offsets() {
914 let mut udt = UserDefinedType::new("TestUDT".to_string());
915
916 udt.add_member(UdtMember {
917 name: "Bool1".to_string(),
918 data_type: 0x00C1,
919 offset: 0,
920 size: 1,
921 });
922
923 udt.add_member(UdtMember {
924 name: "Dint1".to_string(),
925 data_type: 0x00C4,
926 offset: 4,
927 size: 4,
928 });
929
930 assert_eq!(udt.get_member_offset("Bool1"), Some(0));
931 assert_eq!(udt.get_member_offset("Dint1"), Some(4));
932 assert_eq!(udt.size, 8);
933 }
934
935 #[test]
936 fn test_udt_parsing() {
937 let mut udt = UserDefinedType::new("TestUDT".to_string());
938
939 udt.add_member(UdtMember {
940 name: "Bool1".to_string(),
941 data_type: 0x00C1,
942 offset: 0,
943 size: 1,
944 });
945
946 udt.add_member(UdtMember {
947 name: "Dint1".to_string(),
948 data_type: 0x00C4,
949 offset: 4,
950 size: 4,
951 });
952
953 let data = vec![0xFF, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00];
954 let result = udt.to_hash_map(&data).unwrap();
955
956 assert_eq!(result.get("Bool1"), Some(&PlcValue::Bool(true)));
957 assert_eq!(result.get("Dint1"), Some(&PlcValue::Dint(42)));
958 }
959
960 #[test]
961 fn to_hash_map_errors_on_truncated_member_data() {
962 let mut udt = UserDefinedType::new("TestUDT".to_string());
963 udt.add_member(UdtMember {
964 name: "Dint1".to_string(),
965 data_type: 0x00C4,
966 offset: 0,
967 size: 4,
968 });
969 udt.add_member(UdtMember {
970 name: "Dint2".to_string(),
971 data_type: 0x00C4,
972 offset: 4,
973 size: 4,
974 });
975
976 let error = udt.to_hash_map(&[1, 0, 0, 0, 2, 0]).unwrap_err();
977 assert!(error.to_string().contains("Dint2 data incomplete"));
978 }
979
980 #[test]
981 fn from_hash_map_errors_on_missing_member() {
982 let mut udt = UserDefinedType::new("TestUDT".to_string());
983 udt.add_member(UdtMember {
984 name: "Dint1".to_string(),
985 data_type: 0x00C4,
986 offset: 0,
987 size: 4,
988 });
989 udt.add_member(UdtMember {
990 name: "Dint2".to_string(),
991 data_type: 0x00C4,
992 offset: 4,
993 size: 4,
994 });
995
996 let values = HashMap::from([("Dint1".to_string(), PlcValue::Dint(1))]);
997 let error = udt.from_hash_map(&values).unwrap_err();
998 assert!(
999 error
1000 .to_string()
1001 .contains("Missing UDT member value: Dint2")
1002 );
1003 }
1004
1005 #[test]
1006 fn packed_bool_members_use_template_info_bit_index() {
1007 let mut udt = UserDefinedType::new("PackedBoolUDT".to_string());
1008 udt.add_member_with_bit_index(
1009 UdtMember {
1010 name: "Bit0".to_string(),
1011 data_type: 0x00C1,
1012 offset: 0,
1013 size: 1,
1014 },
1015 Some(0),
1016 );
1017 udt.add_member_with_bit_index(
1018 UdtMember {
1019 name: "Bit3".to_string(),
1020 data_type: 0x00C1,
1021 offset: 0,
1022 size: 1,
1023 },
1024 Some(3),
1025 );
1026
1027 let parsed = udt.to_hash_map(&[0b0000_1000]).unwrap();
1028 assert_eq!(parsed.get("Bit0"), Some(&PlcValue::Bool(false)));
1029 assert_eq!(parsed.get("Bit3"), Some(&PlcValue::Bool(true)));
1030
1031 let encoded = udt
1032 .from_hash_map(&HashMap::from([
1033 ("Bit0".to_string(), PlcValue::Bool(true)),
1034 ("Bit3".to_string(), PlcValue::Bool(true)),
1035 ]))
1036 .unwrap();
1037 assert_eq!(encoded, vec![0b0000_1001]);
1038 }
1039
1040 #[test]
1041 fn test_from_cip_data_returns_explicit_error_until_implemented() {
1042 let result = UserDefinedType::from_cip_data(&[0x01, 0x02, 0x03]);
1043 assert!(result.is_err());
1044 let error_text = result.err().unwrap().to_string();
1045 assert!(error_text.contains("not implemented"));
1046 }
1047
1048 #[test]
1049 fn test_serialize_udt_instance_returns_explicit_error_until_implemented() {
1050 let manager = UdtManager::new();
1051 let values = HashMap::new();
1052 let result = manager.serialize_udt_instance(&values);
1053 assert!(result.is_err());
1054 let error_text = result.err().unwrap().to_string();
1055 assert!(error_text.contains("not implemented"));
1056 }
1057
1058 #[test]
1059 fn test_parse_udt_template_reads_live_style_member_records() {
1060 let manager = UdtManager::new();
1061 let data = vec![
1062 0x00, 0x00, 0xC4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0xCA, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0xC2, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, 0x00, 0x08, 0x00, 0x00, 0x00, b'T', b'E', b'S', b'T', b'_', b'U', b'D', b'T', b';', b'n', 0x00, b'M', b'e', b'm',
1067 b'b', b'e', b'r', b'1', 0x00, b'M', b'e', b'm', b'b', b'e', b'r', b'2', 0x00, b'Z',
1068 b'Z', b'Z', b'Z', b'Z', b'Z', b'Z', b'Z', b'Z', b'Z', 0x00, b'F', b'l', b'a', b'g',
1069 0x00,
1070 ];
1071
1072 let template = manager
1073 .parse_udt_template(123, 4, 12, &data)
1074 .expect("template should parse");
1075
1076 assert_eq!(template.name, "TEST_UDT");
1077 assert_eq!(template.members.len(), 3);
1078 assert_eq!(template.members[0].name, "Member1");
1079 assert_eq!(template.members[0].data_type, 0x00C4);
1080 assert_eq!(template.members[0].offset, 0);
1081 assert_eq!(template.members[1].name, "Member2");
1082 assert_eq!(template.members[1].data_type, 0x00CA);
1083 assert_eq!(template.members[1].offset, 4);
1084 assert_eq!(template.members[2].name, "Flag");
1085 assert_eq!(template.members[2].data_type, 0x00C1);
1086 assert_eq!(template.members[2].offset, 8);
1087 assert_eq!(template.members[2].size, 1);
1088 }
1089
1090 #[test]
1091 fn test_parse_udt_template_preserves_empty_member_names_positionally() {
1092 let manager = UdtManager::new();
1093 let data = vec![
1094 0x00, 0x00, 0xC4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC2, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0xC1, 0x00, 0x04, 0x00, 0x00, 0x00, b'T', b'E', b'S', b'T', b'_', b'U', b'D', b'T', 0x00, b'C', b'o', b'u', b'n', b't',
1101 0x00, 0x00, b'F', b'l', b'a', b'g', 0x00,
1102 ];
1103
1104 let template = manager
1105 .parse_udt_template(321, 3, 5, &data)
1106 .expect("template should parse");
1107
1108 assert_eq!(template.members.len(), 2);
1109 assert_eq!(template.members[0].name, "Count");
1110 assert_eq!(template.members[1].name, "Flag");
1111 assert_eq!(template.members[1].offset, 4);
1112 }
1113}