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