Skip to main content

rust_ethernet_ip_udt/
lib.rs

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/// Definition of a User Defined Type
34#[derive(Debug, Clone)]
35pub struct UdtDefinition {
36    pub name: String,
37    pub members: Vec<UdtMember>,
38}
39
40/// Member of a UDT
41#[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/// UDT Template information from PLC
50#[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/// Tag attributes from PLC
60#[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/// Tag permissions
80#[derive(Debug, Clone, PartialEq)]
81pub enum TagPermissions {
82    ReadOnly,
83    ReadWrite,
84    WriteOnly,
85    Unknown,
86}
87
88/// Tag scope
89#[derive(Debug, Clone, PartialEq)]
90pub enum TagScope {
91    Controller,
92    Program(String),
93    Unknown,
94}
95
96/// Manager for UDT operations
97#[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    /// Adds a UDT definition to the cache
114    pub fn add_definition(&mut self, definition: UdtDefinition) {
115        self.definitions.insert(definition.name.clone(), definition);
116    }
117
118    /// Gets a cached UDT definition
119    pub fn get_definition(&self, name: &str) -> Option<&UdtDefinition> {
120        self.definitions.get(name)
121    }
122
123    /// Adds a UDT template to the cache
124    pub fn add_template(&mut self, template: UdtTemplate) {
125        self.templates.insert(template.template_id, template);
126    }
127
128    /// Gets a cached UDT template
129    pub fn get_template(&self, template_id: u32) -> Option<&UdtTemplate> {
130        self.templates.get(&template_id)
131    }
132
133    /// Adds tag attributes to the cache
134    pub fn add_tag_attributes(&mut self, attributes: TagAttributes) {
135        self.tag_attributes
136            .insert(attributes.name.clone(), attributes);
137    }
138
139    /// Gets cached tag attributes
140    pub fn get_tag_attributes(&self, name: &str) -> Option<&TagAttributes> {
141        self.tag_attributes.get(name)
142    }
143
144    /// Lists all cached UDT definitions
145    pub fn list_definitions(&self) -> Vec<String> {
146        self.definitions.keys().cloned().collect()
147    }
148
149    /// Lists all cached templates
150    pub fn list_templates(&self) -> Vec<u32> {
151        self.templates.keys().cloned().collect()
152    }
153
154    /// Lists all cached tag attributes
155    pub fn list_tag_attributes(&self) -> Vec<String> {
156        self.tag_attributes.keys().cloned().collect()
157    }
158
159    /// Clears all caches
160    pub fn clear_cache(&mut self) {
161        self.definitions.clear();
162        self.templates.clear();
163        self.tag_attributes.clear();
164    }
165
166    /// Parses UDT template data from the Template Read service payload.
167    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    /// Gets the size of a data type in bytes
298    fn get_data_type_size(&self, data_type: u16) -> u32 {
299        match data_type {
300            0x00C1 => 1,  // BOOL
301            0x00C2 => 1,  // SINT (8-bit signed)
302            0x00C3 => 2,  // INT (16-bit signed)
303            0x00C4 => 4,  // DINT (32-bit signed)
304            0x00C5 => 8,  // LINT (64-bit signed)
305            0x00C6 => 1,  // USINT (8-bit unsigned)
306            0x00C7 => 2,  // UINT (16-bit unsigned)
307            0x00C8 => 4,  // UDINT (32-bit unsigned)
308            0x00C9 => 8,  // ULINT (64-bit unsigned)
309            0x00CA => 4,  // REAL (32-bit float)
310            0x00CB => 8,  // LREAL (64-bit float)
311            0x00CE => 88, // STRING (4-byte DINT length + 82 chars + 2 padding)
312            _ => 4,       // Default to 4 bytes for unknown types
313        }
314    }
315
316    /// Parses tag attributes from CIP response
317    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        // Parse data type
327        let data_type = u16::from_le_bytes([data[offset], data[offset + 1]]);
328        offset += 2;
329
330        // Parse size
331        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        // Parse dimensions (if present)
340        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        // Parse permissions (simplified - would need more CIP data)
360        let permissions = TagPermissions::ReadWrite; // Default assumption
361
362        // Parse scope (simplified - would need more CIP data)
363        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        // Get data type name
375        let data_type_name = self.get_data_type_name(data_type);
376
377        // Check if this is a UDT (has template instance ID)
378        let template_instance_id = if data_type == 0x00A0 {
379            // UDT type
380            Some(0) // Would need to extract from additional CIP data
381        } 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    /// Gets the human-readable name of a data type
398    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    /// Parse a UDT instance from raw bytes
418    ///
419    /// Returns raw UDT data in generic format. Note: symbol_id will be 0
420    /// since it's not available in this context. For proper UDT handling with
421    /// symbol_id, use read_tag() which gets tag attributes first.
422    pub fn parse_udt_instance(&self, _udt_name: &str, data: &[u8]) -> Result<PlcValue> {
423        // Return raw UDT data in generic format
424        // symbol_id is 0 since it's not available in this context
425        Ok(PlcValue::Udt(UdtData {
426            symbol_id: 0, // Not available in this context
427            data: data.to_vec(),
428        }))
429    }
430
431    /// Serialize a UDT instance to bytes
432    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// Note: Types are already defined above, no need to re-export
449
450/// Represents a User Defined Type (UDT)
451#[derive(Debug, Clone)]
452pub struct UserDefinedType {
453    /// Name of the UDT
454    pub name: String,
455    /// Total size of the UDT in bytes
456    pub size: u32,
457    /// Members of the UDT
458    pub members: Vec<UdtMember>,
459    /// Cache of member offsets for quick lookup
460    member_offsets: HashMap<String, u32>,
461    /// Optional Logix BOOL bit index by member name for packed BOOL members.
462    member_bit_indices: HashMap<String, u8>,
463}
464
465impl UserDefinedType {
466    /// Creates a new UDT
467    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    /// Adds a member to the UDT
478    pub fn add_member(&mut self, member: UdtMember) {
479        self.add_member_with_bit_index(member, None);
480    }
481
482    /// Adds a member with optional packed-BOOL bit metadata.
483    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        // Calculate total size including padding
492        self.size = self
493            .members
494            .iter()
495            .map(|m| m.offset + m.size)
496            .max()
497            .unwrap_or(0);
498    }
499
500    /// Gets the offset of a member by name
501    pub fn get_member_offset(&self, name: &str) -> Option<u32> {
502        self.member_offsets.get(name).copied()
503    }
504
505    /// Parses a UDT from CIP data
506    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    /// Converts a UDT instance to a `HashMap` of member values
513    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    /// Converts a `HashMap` of member values to raw UDT bytes
542    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    /// Reads a specific UDT member by name
596    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    /// Writes a specific UDT member by name
617    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    /// Gets the size of a specific member
641    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    /// Gets the data type of a specific member
649    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    /// Parses a member value from raw data
657    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                // SINT (8-bit signed integer)
673                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                // INT (16-bit signed integer)
680                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                // DINT (32-bit signed integer)
689                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                // LINT (64-bit signed integer)
698                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                // USINT (8-bit unsigned integer)
707                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                // UINT (16-bit unsigned integer)
714                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                // UDINT (32-bit unsigned integer)
723                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                // ULINT (64-bit unsigned integer)
732                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                // REAL (32-bit float)
741                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                // LREAL (64-bit float)
750                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                // STRING type - first 4 bytes are length (DINT), followed by data (up to 82 bytes)
759                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    /// Serializes a member value to raw data
778    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); // Subtract 4 for DINT length field
862                        let max_chars = (max_data_len as usize).min(82); // Max STRING length is 82
863                        let length = (s.len() as u32).min(max_chars as u32);
864                        // Length field is 4 bytes (DINT)
865                        result.extend_from_slice(&length.to_le_bytes());
866                        result.extend_from_slice(&s.as_bytes()[..length as usize]);
867                        // Pad to even byte boundary, but don't exceed member size
868                        while result.len() < member.size as usize && result.len() % 2 != 0 {
869                            result.push(0);
870                        }
871                        // Ensure we don't exceed member size
872                        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, // DINT @ 0
1063            0x04, 0x00, 0xCA, 0x00, 0x04, 0x00, 0x00, 0x00, // REAL @ 4
1064            0x08, 0x00, 0xC2, 0x00, 0x08, 0x00, 0x00, 0x00, // hidden SINT host @ 8
1065            0x00, 0x00, 0xC1, 0x00, 0x08, 0x00, 0x00, 0x00, // BOOL @ 8
1066            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, // DINT @ 0
1095            0x00, 0x00, 0xC2, 0x00, 0x04, 0x00, 0x00, 0x00, // hidden host @ 4
1096            0x01, 0x00, 0xC1, 0x00, 0x04, 0x00, 0x00, 0x00, // BOOL bit 1 @ 4
1097            // Template strings are a NUL-separated template name followed by
1098            // one member name per member record. The empty middle name models
1099            // hidden/host members and must not shift following names left.
1100            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}