Skip to main content

rust_ethernet_ip_udt/
lib.rs

1//! UDT template parsing, cached metadata, and member value conversion.
2
3use rust_ethernet_ip_types::{PlcValue, TypeError, UdtCodec, UdtData};
4
5/// Result type returned by UDT operations.
6pub type Result<T> = std::result::Result<T, UdtError>;
7
8/// Error returned while parsing or converting a UDT.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum UdtError {
11    /// The PLC response or UDT payload is malformed.
12    Protocol(String),
13    /// The requested tag was not found.
14    TagNotFound(String),
15    /// A member value has a different type than its definition.
16    DataTypeMismatch {
17        /// Data type required by the UDT definition.
18        expected: String,
19        /// Data type supplied by the caller.
20        actual: String,
21    },
22}
23
24impl UdtError {
25    /// Creates a protocol-format error.
26    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/// Definition of a User Defined Type
47#[derive(Debug, Clone)]
48pub struct UdtDefinition {
49    /// Logix data type name.
50    pub name: String,
51    /// Members in template order.
52    pub members: Vec<UdtMember>,
53}
54
55/// Member of a UDT
56#[derive(Debug, Clone)]
57pub struct UdtMember {
58    /// Member name.
59    pub name: String,
60    /// CIP data type code.
61    pub data_type: u16,
62    /// Byte offset from the start of the structure.
63    pub offset: u32,
64    /// Encoded member size in bytes.
65    pub size: u32,
66}
67
68/// UDT Template information from PLC
69#[derive(Debug, Clone)]
70pub struct UdtTemplate {
71    /// Template object instance id.
72    pub template_id: u32,
73    /// Logix data type name.
74    pub name: String,
75    /// Encoded structure size in bytes.
76    pub size: u32,
77    /// Member count reported by the controller.
78    pub member_count: u16,
79    /// Parsed, named members.
80    pub members: Vec<UdtMember>,
81}
82
83/// Tag attributes from PLC
84#[derive(Debug, Clone)]
85pub struct TagAttributes {
86    /// Fully qualified symbolic tag name.
87    pub name: String,
88    /// CIP data type code.
89    pub data_type: u16,
90    /// Human-readable data type name.
91    pub data_type_name: String,
92    /// Declared array dimensions, or an empty vector for a scalar.
93    pub dimensions: Vec<u32>,
94    /// Reported access permissions.
95    pub permissions: TagPermissions,
96    /// Controller or program scope.
97    pub scope: TagScope,
98    /// Template instance id for a structured value, when known.
99    pub template_instance_id: Option<u32>,
100    /// Encoded tag size in bytes.
101    pub size: u32,
102}
103
104#[derive(Clone, Copy)]
105struct RawMember {
106    info: u16,
107    raw_type: u16,
108    offset: u32,
109}
110
111/// Tag permissions
112#[derive(Debug, Clone, PartialEq)]
113pub enum TagPermissions {
114    /// Reads are allowed and writes are not.
115    ReadOnly,
116    /// Reads and writes are allowed.
117    ReadWrite,
118    /// Writes are allowed and reads are not.
119    WriteOnly,
120    /// Permissions were not reported or could not be inferred.
121    Unknown,
122}
123
124/// Tag scope
125#[derive(Debug, Clone, PartialEq)]
126pub enum TagScope {
127    /// Controller-scoped tag.
128    Controller,
129    /// Program-scoped tag carrying the program name.
130    Program(String),
131    /// Scope was not reported or could not be inferred.
132    Unknown,
133}
134
135/// Manager for UDT operations
136#[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    /// Creates an empty UDT metadata cache.
145    pub fn new() -> Self {
146        Self {
147            definitions: HashMap::new(),
148            templates: HashMap::new(),
149            tag_attributes: HashMap::new(),
150        }
151    }
152
153    /// Adds a UDT definition to the cache
154    pub fn add_definition(&mut self, definition: UdtDefinition) {
155        self.definitions.insert(definition.name.clone(), definition);
156    }
157
158    /// Gets a cached UDT definition
159    pub fn get_definition(&self, name: &str) -> Option<&UdtDefinition> {
160        self.definitions.get(name)
161    }
162
163    /// Adds a UDT template to the cache
164    pub fn add_template(&mut self, template: UdtTemplate) {
165        self.templates.insert(template.template_id, template);
166    }
167
168    /// Gets a cached UDT template
169    pub fn get_template(&self, template_id: u32) -> Option<&UdtTemplate> {
170        self.templates.get(&template_id)
171    }
172
173    /// Adds tag attributes to the cache
174    pub fn add_tag_attributes(&mut self, attributes: TagAttributes) {
175        self.tag_attributes
176            .insert(attributes.name.clone(), attributes);
177    }
178
179    /// Gets cached tag attributes
180    pub fn get_tag_attributes(&self, name: &str) -> Option<&TagAttributes> {
181        self.tag_attributes.get(name)
182    }
183
184    /// Lists all cached UDT definitions
185    pub fn list_definitions(&self) -> Vec<String> {
186        self.definitions.keys().cloned().collect()
187    }
188
189    /// Lists all cached templates
190    pub fn list_templates(&self) -> Vec<u32> {
191        self.templates.keys().cloned().collect()
192    }
193
194    /// Lists all cached tag attributes
195    pub fn list_tag_attributes(&self) -> Vec<String> {
196        self.tag_attributes.keys().cloned().collect()
197    }
198
199    /// Clears all caches
200    pub fn clear_cache(&mut self) {
201        self.definitions.clear();
202        self.templates.clear();
203        self.tag_attributes.clear();
204    }
205
206    /// Parses UDT template data from the Template Read service payload.
207    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    /// Gets the size of a data type in bytes
338    fn get_data_type_size(&self, data_type: u16) -> u32 {
339        match data_type {
340            0x00C1 => 1,  // BOOL
341            0x00C2 => 1,  // SINT (8-bit signed)
342            0x00C3 => 2,  // INT (16-bit signed)
343            0x00C4 => 4,  // DINT (32-bit signed)
344            0x00C5 => 8,  // LINT (64-bit signed)
345            0x00C6 => 1,  // USINT (8-bit unsigned)
346            0x00C7 => 2,  // UINT (16-bit unsigned)
347            0x00C8 => 4,  // UDINT (32-bit unsigned)
348            0x00C9 => 8,  // ULINT (64-bit unsigned)
349            0x00CA => 4,  // REAL (32-bit float)
350            0x00CB => 8,  // LREAL (64-bit float)
351            0x00CE => 88, // STRING (4-byte DINT length + 82 chars + 2 padding)
352            _ => 4,       // Default to 4 bytes for unknown types
353        }
354    }
355
356    /// Parses tag attributes from CIP response
357    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        // Parse data type
367        let data_type = u16::from_le_bytes([data[offset], data[offset + 1]]);
368        offset += 2;
369
370        // Parse size
371        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        // Parse dimensions (if present)
380        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        // Parse permissions (simplified - would need more CIP data)
400        let permissions = TagPermissions::ReadWrite; // Default assumption
401
402        // Parse scope (simplified - would need more CIP data)
403        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        // Get data type name
415        let data_type_name = self.get_data_type_name(data_type);
416
417        // Check if this is a UDT (has template instance ID)
418        let template_instance_id = if data_type == 0x00A0 {
419            // UDT type
420            Some(0) // Would need to extract from additional CIP data
421        } 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    /// Gets the human-readable name of a data type
438    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    /// Parse a UDT instance from raw bytes
458    ///
459    /// Returns raw UDT data in generic format. Note: symbol_id will be 0
460    /// since it's not available in this context. For proper UDT handling with
461    /// symbol_id, use read_tag() which gets tag attributes first.
462    pub fn parse_udt_instance(&self, _udt_name: &str, data: &[u8]) -> Result<PlcValue> {
463        // Return raw UDT data in generic format
464        // symbol_id is 0 since it's not available in this context
465        Ok(PlcValue::Udt(UdtData {
466            symbol_id: 0, // Not available in this context
467            data: data.to_vec(),
468        }))
469    }
470
471    /// Serialize a UDT instance to bytes
472    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// Note: Types are already defined above, no need to re-export
489
490/// Represents a User Defined Type (UDT)
491#[derive(Debug, Clone)]
492pub struct UserDefinedType {
493    /// Name of the UDT
494    pub name: String,
495    /// Total size of the UDT in bytes
496    pub size: u32,
497    /// Members of the UDT
498    pub members: Vec<UdtMember>,
499    /// Cache of member offsets for quick lookup
500    member_offsets: HashMap<String, u32>,
501    /// Optional Logix BOOL bit index by member name for packed BOOL members.
502    member_bit_indices: HashMap<String, u8>,
503}
504
505impl UserDefinedType {
506    /// Creates a new UDT
507    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    /// Adds a member to the UDT
518    pub fn add_member(&mut self, member: UdtMember) {
519        self.add_member_with_bit_index(member, None);
520    }
521
522    /// Adds a member with optional packed-BOOL bit metadata.
523    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        // Calculate total size including padding
532        self.size = self
533            .members
534            .iter()
535            .map(|m| m.offset + m.size)
536            .max()
537            .unwrap_or(0);
538    }
539
540    /// Gets the offset of a member by name
541    pub fn get_member_offset(&self, name: &str) -> Option<u32> {
542        self.member_offsets.get(name).copied()
543    }
544
545    /// Parses a UDT from CIP data
546    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    /// Converts a UDT instance to a `HashMap` of member values
553    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    /// Converts a `HashMap` of member values to raw UDT bytes
582    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    /// Reads a specific UDT member by name
636    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    /// Writes a specific UDT member by name
657    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    /// Gets the size of a specific member
681    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    /// Gets the data type of a specific member
689    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    /// Parses a member value from raw data
697    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                // SINT (8-bit signed integer)
713                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                // INT (16-bit signed integer)
720                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                // DINT (32-bit signed integer)
729                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                // LINT (64-bit signed integer)
738                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                // USINT (8-bit unsigned integer)
747                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                // UINT (16-bit unsigned integer)
754                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                // UDINT (32-bit unsigned integer)
763                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                // ULINT (64-bit unsigned integer)
772                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                // REAL (32-bit float)
781                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                // LREAL (64-bit float)
790                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                // STRING type - first 4 bytes are length (DINT), followed by data (up to 82 bytes)
799                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    /// Serializes a member value to raw data
818    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); // Subtract 4 for DINT length field
902                        let max_chars = (max_data_len as usize).min(82); // Max STRING length is 82
903                        let length = (s.len() as u32).min(max_chars as u32);
904                        // Length field is 4 bytes (DINT)
905                        result.extend_from_slice(&length.to_le_bytes());
906                        result.extend_from_slice(&s.as_bytes()[..length as usize]);
907                        // Pad to even byte boundary, but don't exceed member size
908                        while result.len() < member.size as usize && result.len() % 2 != 0 {
909                            result.push(0);
910                        }
911                        // Ensure we don't exceed member size
912                        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, // DINT @ 0
1103            0x04, 0x00, 0xCA, 0x00, 0x04, 0x00, 0x00, 0x00, // REAL @ 4
1104            0x08, 0x00, 0xC2, 0x00, 0x08, 0x00, 0x00, 0x00, // hidden SINT host @ 8
1105            0x00, 0x00, 0xC1, 0x00, 0x08, 0x00, 0x00, 0x00, // BOOL @ 8
1106            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, // DINT @ 0
1135            0x00, 0x00, 0xC2, 0x00, 0x04, 0x00, 0x00, 0x00, // hidden host @ 4
1136            0x01, 0x00, 0xC1, 0x00, 0x04, 0x00, 0x00, 0x00, // BOOL bit 1 @ 4
1137            // Template strings are a NUL-separated template name followed by
1138            // one member name per member record. The empty middle name models
1139            // hidden/host members and must not shift following names left.
1140            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}