rust_ethernet_ip/
tag_path.rs

1// tag_path.rs - Advanced Tag Path Parsing for Allen-Bradley PLCs
2// =========================================================================
3//
4// This module provides comprehensive tag path parsing and generation for
5// Allen-Bradley CompactLogix and ControlLogix PLCs, supporting:
6//
7// - Program-scoped tags: "Program:MainProgram.Tag1"
8// - Array elements: "MyArray[5]", "MyArray[1,2,3]"
9// - Bit access: "MyDINT.15" (access individual bits)
10// - UDT members: "MyUDT.Member1.SubMember"
11// - String operations: "MyString.LEN", "MyString.DATA[5]"
12//
13// =========================================================================
14
15use std::fmt;
16use crate::error::{EtherNetIpError, Result};
17
18/// Represents different types of tag addressing supported by Allen-Bradley PLCs
19#[derive(Debug, Clone, PartialEq)]
20pub enum TagPath {
21    /// Simple controller-scoped tag: "MyTag"
22    Controller {
23        tag_name: String,
24    },
25    
26    /// Program-scoped tag: "Program:MainProgram.MyTag"
27    Program {
28        program_name: String,
29        tag_name: String,
30    },
31    
32    /// Array element access: "MyArray[5]" or "MyArray[1,2,3]"
33    Array {
34        base_path: Box<TagPath>,
35        indices: Vec<u32>,
36    },
37    
38    /// Bit access within a tag: "MyDINT.15"
39    Bit {
40        base_path: Box<TagPath>,
41        bit_index: u8,
42    },
43    
44    /// UDT member access: "MyUDT.Member1"
45    Member {
46        base_path: Box<TagPath>,
47        member_name: String,
48    },
49    
50    /// String length access: "MyString.LEN"
51    StringLength {
52        base_path: Box<TagPath>,
53    },
54    
55    /// String data access: "MyString.DATA[5]"
56    StringData {
57        base_path: Box<TagPath>,
58        index: u32,
59    },
60}
61
62impl TagPath {
63    /// Parses a tag path string into a structured TagPath
64    /// 
65    /// # Examples
66    /// 
67    /// ```rust
68    /// use rust_ethernet_ip::TagPath;
69    /// 
70    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
71    /// // Simple controller tag
72    /// let path = TagPath::parse("MyTag")?;
73    /// 
74    /// // Program-scoped tag
75    /// let path = TagPath::parse("Program:MainProgram.MyTag")?;
76    /// 
77    /// // Array element
78    /// let path = TagPath::parse("MyArray[5]")?;
79    /// 
80    /// // Multi-dimensional array
81    /// let path = TagPath::parse("Matrix[1,2,3]")?;
82    /// 
83    /// // Bit access
84    /// let path = TagPath::parse("StatusWord.15")?;
85    /// 
86    /// // UDT member
87    /// let path = TagPath::parse("MotorData.Speed")?;
88    /// 
89    /// // Complex nested path
90    /// let path = TagPath::parse("Program:Safety.Devices[2].Status.15")?;
91    /// # Ok(())
92    /// # }
93    /// ```
94    pub fn parse(path_str: &str) -> Result<Self> {
95        let parser = TagPathParser::new(path_str);
96        parser.parse()
97    }
98    
99    /// Converts the TagPath back to a string representation
100    pub fn to_string(&self) -> String {
101        match self {
102            TagPath::Controller { tag_name } => tag_name.clone(),
103            TagPath::Program { program_name, tag_name } => {
104                format!("Program:{}.{}", program_name, tag_name)
105            }
106            TagPath::Array { base_path, indices } => {
107                let base = base_path.to_string();
108                let indices_str = indices.iter()
109                    .map(|i| i.to_string())
110                    .collect::<Vec<_>>()
111                    .join(",");
112                format!("{}[{}]", base, indices_str)
113            }
114            TagPath::Bit { base_path, bit_index } => {
115                format!("{}.{}", base_path.to_string(), bit_index)
116            }
117            TagPath::Member { base_path, member_name } => {
118                format!("{}.{}", base_path.to_string(), member_name)
119            }
120            TagPath::StringLength { base_path } => {
121                format!("{}.LEN", base_path.to_string())
122            }
123            TagPath::StringData { base_path, index } => {
124                format!("{}.DATA[{}]", base_path.to_string(), index)
125            }
126        }
127    }
128    
129    /// Generates the CIP path bytes for this tag path
130    /// 
131    /// This converts the structured tag path into the binary format
132    /// required by the CIP protocol for EtherNet/IP communication.
133    pub fn to_cip_path(&self) -> Result<Vec<u8>> {
134        let mut path = Vec::new();
135        self.build_cip_path(&mut path)?;
136        
137        // Pad to even length if necessary
138        if path.len() % 2 != 0 {
139            path.push(0x00);
140        }
141        
142        Ok(path)
143    }
144    
145    /// Recursively builds the CIP path bytes
146    fn build_cip_path(&self, path: &mut Vec<u8>) -> Result<()> {
147        match self {
148            TagPath::Controller { tag_name } => {
149                // ANSI Extended Symbol Segment
150                path.push(0x91);
151                path.push(tag_name.len() as u8);
152                path.extend_from_slice(tag_name.as_bytes());
153            }
154            
155            TagPath::Program { program_name, tag_name } => {
156                // Program scope requires special handling
157                // First add program name segment
158                path.push(0x91);
159                let program_path = format!("Program:{}", program_name);
160                path.push(program_path.len() as u8);
161                path.extend_from_slice(program_path.as_bytes());
162                
163                // Then add tag name segment
164                path.push(0x91);
165                path.push(tag_name.len() as u8);
166                path.extend_from_slice(tag_name.as_bytes());
167            }
168            
169            TagPath::Array { base_path, indices } => {
170                // Build base path first
171                base_path.build_cip_path(path)?;
172                
173                // Add array indices
174                for &index in indices {
175                    path.push(0x28); // Element segment
176                    path.extend_from_slice(&index.to_le_bytes());
177                }
178            }
179            
180            TagPath::Bit { base_path, bit_index } => {
181                // Build base path first
182                base_path.build_cip_path(path)?;
183                
184                // Add bit segment
185                path.push(0x29); // Bit segment
186                path.push(*bit_index);
187            }
188            
189            TagPath::Member { base_path, member_name } => {
190                // Build base path first
191                base_path.build_cip_path(path)?;
192                
193                // Add member segment
194                path.push(0x91);
195                path.push(member_name.len() as u8);
196                path.extend_from_slice(member_name.as_bytes());
197            }
198            
199            TagPath::StringLength { base_path } => {
200                // Build base path first
201                base_path.build_cip_path(path)?;
202                
203                // Add LEN member
204                path.push(0x91);
205                path.push(3); // "LEN".len()
206                path.extend_from_slice(b"LEN");
207            }
208            
209            TagPath::StringData { base_path, index } => {
210                // Build base path first
211                base_path.build_cip_path(path)?;
212                
213                // Add DATA member
214                path.push(0x91);
215                path.push(4); // "DATA".len()
216                path.extend_from_slice(b"DATA");
217                
218                // Add array index
219                path.push(0x28); // Element segment
220                path.extend_from_slice(&index.to_le_bytes());
221            }
222        }
223        
224        Ok(())
225    }
226    
227    /// Returns the base tag name without any path qualifiers
228    pub fn base_tag_name(&self) -> String {
229        match self {
230            TagPath::Controller { tag_name } => tag_name.clone(),
231            TagPath::Program { tag_name, .. } => tag_name.clone(),
232            TagPath::Array { base_path, .. } => base_path.base_tag_name(),
233            TagPath::Bit { base_path, .. } => base_path.base_tag_name(),
234            TagPath::Member { base_path, .. } => base_path.base_tag_name(),
235            TagPath::StringLength { base_path } => base_path.base_tag_name(),
236            TagPath::StringData { base_path, .. } => base_path.base_tag_name(),
237        }
238    }
239    
240    /// Returns true if this is a program-scoped tag
241    pub fn is_program_scoped(&self) -> bool {
242        match self {
243            TagPath::Program { .. } => true,
244            TagPath::Array { base_path, .. } => base_path.is_program_scoped(),
245            TagPath::Bit { base_path, .. } => base_path.is_program_scoped(),
246            TagPath::Member { base_path, .. } => base_path.is_program_scoped(),
247            TagPath::StringLength { base_path } => base_path.is_program_scoped(),
248            TagPath::StringData { base_path, .. } => base_path.is_program_scoped(),
249            _ => false,
250        }
251    }
252    
253    /// Returns the program name if this is a program-scoped tag
254    pub fn program_name(&self) -> Option<String> {
255        match self {
256            TagPath::Program { program_name, .. } => Some(program_name.clone()),
257            TagPath::Array { base_path, .. } => base_path.program_name(),
258            TagPath::Bit { base_path, .. } => base_path.program_name(),
259            TagPath::Member { base_path, .. } => base_path.program_name(),
260            TagPath::StringLength { base_path } => base_path.program_name(),
261            TagPath::StringData { base_path, .. } => base_path.program_name(),
262            _ => None,
263        }
264    }
265}
266
267impl fmt::Display for TagPath {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        write!(f, "{}", self.to_string())
270    }
271}
272
273/// Internal parser for tag path strings
274struct TagPathParser<'a> {
275    input: &'a str,
276    position: usize,
277}
278
279impl<'a> TagPathParser<'a> {
280    fn new(input: &'a str) -> Self {
281        Self { input, position: 0 }
282    }
283    
284    fn parse(mut self) -> Result<TagPath> {
285        self.parse_path()
286    }
287    
288    fn parse_path(&mut self) -> Result<TagPath> {
289        // Check for program scope
290        if self.input.starts_with("Program:") {
291            self.parse_program_scoped()
292        } else {
293            self.parse_controller_scoped()
294        }
295    }
296    
297    fn parse_program_scoped(&mut self) -> Result<TagPath> {
298        // Skip "Program:"
299        self.position = 8;
300        
301        // Parse program name (until first dot)
302        let program_name = self.parse_identifier()?;
303        
304        // Expect dot
305        if !self.consume_char('.') {
306            return Err(EtherNetIpError::Protocol(
307                "Expected '.' after program name".to_string()
308            ));
309        }
310        
311        // Parse tag name
312        let tag_name = self.parse_identifier()?;
313        
314        let mut path = TagPath::Program { program_name, tag_name };
315        
316        // Parse any additional qualifiers (arrays, members, bits)
317        while self.position < self.input.len() {
318            path = self.parse_qualifier(path)?;
319        }
320        
321        Ok(path)
322    }
323    
324    fn parse_controller_scoped(&mut self) -> Result<TagPath> {
325        let tag_name = self.parse_identifier()?;
326        let mut path = TagPath::Controller { tag_name };
327        
328        // Parse any additional qualifiers
329        while self.position < self.input.len() {
330            path = self.parse_qualifier(path)?;
331        }
332        
333        Ok(path)
334    }
335    
336    fn parse_qualifier(&mut self, base_path: TagPath) -> Result<TagPath> {
337        match self.peek_char() {
338            Some('[') => self.parse_array_access(base_path),
339            Some('.') => self.parse_member_or_bit_access(base_path),
340            _ => Err(EtherNetIpError::Protocol(
341                format!("Unexpected character at position {}", self.position)
342            )),
343        }
344    }
345    
346    fn parse_array_access(&mut self, base_path: TagPath) -> Result<TagPath> {
347        // Consume '['
348        self.consume_char('[');
349        
350        let mut indices = Vec::new();
351        
352        // Parse first index
353        indices.push(self.parse_number()?);
354        
355        // Parse additional indices separated by commas
356        while self.peek_char() == Some(',') {
357            self.consume_char(',');
358            indices.push(self.parse_number()?);
359        }
360        
361        // Expect ']'
362        if !self.consume_char(']') {
363            return Err(EtherNetIpError::Protocol(
364                "Expected ']' after array indices".to_string()
365            ));
366        }
367        
368        Ok(TagPath::Array {
369            base_path: Box::new(base_path),
370            indices,
371        })
372    }
373    
374    fn parse_member_or_bit_access(&mut self, base_path: TagPath) -> Result<TagPath> {
375        // Consume '.'
376        self.consume_char('.');
377        
378        // Check for special string operations
379        if self.input[self.position..].starts_with("LEN") {
380            self.position += 3;
381            return Ok(TagPath::StringLength {
382                base_path: Box::new(base_path),
383            });
384        }
385        
386        if self.input[self.position..].starts_with("DATA[") {
387            self.position += 5; // Skip "DATA["
388            let index = self.parse_number()?;
389            if !self.consume_char(']') {
390                return Err(EtherNetIpError::Protocol(
391                    "Expected ']' after DATA index".to_string()
392                ));
393            }
394            return Ok(TagPath::StringData {
395                base_path: Box::new(base_path),
396                index,
397            });
398        }
399        
400        // Parse identifier (could be member name or bit index)
401        let identifier = self.parse_identifier()?;
402        
403        // Check if it's a numeric bit index
404        if let Ok(bit_index) = identifier.parse::<u8>() {
405            if bit_index < 32 { // Valid bit range for DINT
406                return Ok(TagPath::Bit {
407                    base_path: Box::new(base_path),
408                    bit_index,
409                });
410            }
411        }
412        
413        // It's a member name
414        Ok(TagPath::Member {
415            base_path: Box::new(base_path),
416            member_name: identifier,
417        })
418    }
419    
420    fn parse_identifier(&mut self) -> Result<String> {
421        let start = self.position;
422        
423        while self.position < self.input.len() {
424            let ch = self.input.chars().nth(self.position).unwrap();
425            if ch.is_alphanumeric() || ch == '_' {
426                self.position += 1;
427            } else {
428                break;
429            }
430        }
431        
432        if start == self.position {
433            return Err(EtherNetIpError::Protocol(
434                "Expected identifier".to_string()
435            ));
436        }
437        
438        Ok(self.input[start..self.position].to_string())
439    }
440    
441    fn parse_number(&mut self) -> Result<u32> {
442        let start = self.position;
443        
444        while self.position < self.input.len() {
445            let ch = self.input.chars().nth(self.position).unwrap();
446            if ch.is_ascii_digit() {
447                self.position += 1;
448            } else {
449                break;
450            }
451        }
452        
453        if start == self.position {
454            return Err(EtherNetIpError::Protocol(
455                "Expected number".to_string()
456            ));
457        }
458        
459        self.input[start..self.position].parse()
460            .map_err(|_| EtherNetIpError::Protocol("Invalid number".to_string()))
461    }
462    
463    fn peek_char(&self) -> Option<char> {
464        self.input.chars().nth(self.position)
465    }
466    
467    fn consume_char(&mut self, expected: char) -> bool {
468        if self.peek_char() == Some(expected) {
469            self.position += 1;
470            true
471        } else {
472            false
473        }
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480    
481    #[test]
482    fn test_controller_scoped_tag() {
483        let path = TagPath::parse("MyTag").unwrap();
484        assert_eq!(path, TagPath::Controller {
485            tag_name: "MyTag".to_string()
486        });
487        assert_eq!(path.to_string(), "MyTag");
488    }
489    
490    #[test]
491    fn test_program_scoped_tag() {
492        let path = TagPath::parse("Program:MainProgram.MyTag").unwrap();
493        assert_eq!(path, TagPath::Program {
494            program_name: "MainProgram".to_string(),
495            tag_name: "MyTag".to_string()
496        });
497        assert_eq!(path.to_string(), "Program:MainProgram.MyTag");
498        assert!(path.is_program_scoped());
499        assert_eq!(path.program_name(), Some("MainProgram".to_string()));
500    }
501    
502    #[test]
503    fn test_array_access() {
504        let path = TagPath::parse("MyArray[5]").unwrap();
505        if let TagPath::Array { base_path, indices } = path {
506            assert_eq!(*base_path, TagPath::Controller {
507                tag_name: "MyArray".to_string()
508            });
509            assert_eq!(indices, vec![5]);
510        } else {
511            panic!("Expected Array path");
512        }
513    }
514    
515    #[test]
516    fn test_multi_dimensional_array() {
517        let path = TagPath::parse("Matrix[1,2,3]").unwrap();
518        if let TagPath::Array { base_path, indices } = path {
519            assert_eq!(*base_path, TagPath::Controller {
520                tag_name: "Matrix".to_string()
521            });
522            assert_eq!(indices, vec![1, 2, 3]);
523        } else {
524            panic!("Expected Array path");
525        }
526    }
527    
528    #[test]
529    fn test_bit_access() {
530        let path = TagPath::parse("StatusWord.15").unwrap();
531        if let TagPath::Bit { base_path, bit_index } = path {
532            assert_eq!(*base_path, TagPath::Controller {
533                tag_name: "StatusWord".to_string()
534            });
535            assert_eq!(bit_index, 15);
536        } else {
537            panic!("Expected Bit path");
538        }
539    }
540    
541    #[test]
542    fn test_member_access() {
543        let path = TagPath::parse("MotorData.Speed").unwrap();
544        if let TagPath::Member { base_path, member_name } = path {
545            assert_eq!(*base_path, TagPath::Controller {
546                tag_name: "MotorData".to_string()
547            });
548            assert_eq!(member_name, "Speed");
549        } else {
550            panic!("Expected Member path");
551        }
552    }
553    
554    #[test]
555    fn test_string_length() {
556        let path = TagPath::parse("MyString.LEN").unwrap();
557        if let TagPath::StringLength { base_path } = path {
558            assert_eq!(*base_path, TagPath::Controller {
559                tag_name: "MyString".to_string()
560            });
561        } else {
562            panic!("Expected StringLength path");
563        }
564    }
565    
566    #[test]
567    fn test_string_data() {
568        let path = TagPath::parse("MyString.DATA[5]").unwrap();
569        if let TagPath::StringData { base_path, index } = path {
570            assert_eq!(*base_path, TagPath::Controller {
571                tag_name: "MyString".to_string()
572            });
573            assert_eq!(index, 5);
574        } else {
575            panic!("Expected StringData path");
576        }
577    }
578    
579    #[test]
580    fn test_complex_nested_path() {
581        let path = TagPath::parse("Program:Safety.Devices[2].Status.15").unwrap();
582        
583        // This should parse as:
584        // Program:Safety.Devices -> Array[2] -> Member(Status) -> Bit(15)
585        if let TagPath::Bit { base_path, bit_index } = path {
586            assert_eq!(bit_index, 15);
587            
588            if let TagPath::Member { base_path, member_name } = *base_path {
589                assert_eq!(member_name, "Status");
590                
591                if let TagPath::Array { base_path, indices } = *base_path {
592                    assert_eq!(indices, vec![2]);
593                    
594                    if let TagPath::Program { program_name, tag_name } = *base_path {
595                        assert_eq!(program_name, "Safety");
596                        assert_eq!(tag_name, "Devices");
597                    } else {
598                        panic!("Expected Program path");
599                    }
600                } else {
601                    panic!("Expected Array path");
602                }
603            } else {
604                panic!("Expected Member path");
605            }
606        } else {
607            panic!("Expected Bit path");
608        }
609    }
610    
611    #[test]
612    fn test_cip_path_generation() {
613        let path = TagPath::parse("MyTag").unwrap();
614        let cip_path = path.to_cip_path().unwrap();
615        
616        // Should be: [0x91, 0x05, 'M', 'y', 'T', 'a', 'g', 0x00] (padded)
617        assert_eq!(cip_path[0], 0x91); // ANSI Extended Symbol Segment
618        assert_eq!(cip_path[1], 5);    // Length of "MyTag"
619        assert_eq!(&cip_path[2..7], b"MyTag");
620        assert_eq!(cip_path[7], 0x00); // Padding
621    }
622    
623    #[test]
624    fn test_base_tag_name() {
625        let path = TagPath::parse("Program:Main.MotorData[1].Speed.15").unwrap();
626        assert_eq!(path.base_tag_name(), "MotorData");
627    }
628    
629    #[test]
630    fn test_invalid_paths() {
631        assert!(TagPath::parse("").is_err());
632        assert!(TagPath::parse("Program:").is_err());
633        assert!(TagPath::parse("MyArray[").is_err());
634        assert!(TagPath::parse("MyArray]").is_err());
635        assert!(TagPath::parse("MyTag.").is_err());
636    }
637}