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