1use crate::error::{EtherNetIpError, Result};
16use std::fmt;
17
18#[derive(Debug, Clone, PartialEq)]
20pub enum TagPath {
21 Controller { tag_name: String },
23
24 Program {
26 program_name: String,
27 tag_name: String,
28 },
29
30 Array {
32 base_path: Box<TagPath>,
33 indices: Vec<u32>,
34 },
35
36 Bit {
38 base_path: Box<TagPath>,
39 bit_index: u8,
40 },
41
42 Member {
44 base_path: Box<TagPath>,
45 member_name: String,
46 },
47
48 StringLength { base_path: Box<TagPath> },
50
51 StringData { base_path: Box<TagPath>, index: u32 },
53}
54
55impl TagPath {
56 pub fn parse(path_str: &str) -> Result<Self> {
88 let parser = TagPathParser::new(path_str);
89 parser.parse()
90 }
91
92 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(u32::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!("{base_path}.LEN")
125 }
126 TagPath::StringData { base_path, index } => {
127 format!("{base_path}.DATA[{index}]")
128 }
129 }
130 }
131
132 pub fn to_cip_path(&self) -> Result<Vec<u8>> {
137 let mut path = Vec::new();
138 self.build_cip_path(&mut path)?;
139
140 if path.len() % 2 != 0 {
142 path.push(0x00);
143 }
144
145 Ok(path)
146 }
147
148 fn build_cip_path(&self, path: &mut Vec<u8>) -> Result<()> {
150 match self {
151 TagPath::Controller { tag_name } => {
152 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 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 if path.len() % 2 != 0 {
171 path.push(0x00);
172 }
173
174 path.push(0x91);
176 path.push(tag_name.len() as u8);
177 path.extend_from_slice(tag_name.as_bytes());
178 }
179
180 TagPath::Array { base_path, indices } => {
181 base_path.build_cip_path(path)?;
183
184 if path.len() % 2 != 0 {
186 path.push(0x00);
187 }
188
189 for &index in indices {
196 if index <= 255 {
197 path.push(0x28);
199 path.push(index as u8);
200 } else if index <= 65535 {
201 path.push(0x29);
203 path.push(0x00); path.extend_from_slice(&(index as u16).to_le_bytes());
205 } else {
206 path.push(0x2A);
208 path.push(0x00); path.extend_from_slice(&index.to_le_bytes());
210 }
211 }
212 }
213
214 TagPath::Bit {
215 base_path,
216 bit_index,
217 } => {
218 base_path.build_cip_path(path)?;
220
221 if path.len() % 2 != 0 {
223 path.push(0x00);
224 }
225
226 path.push(0x29); path.push(*bit_index);
229 }
230
231 TagPath::Member {
232 base_path,
233 member_name,
234 } => {
235 base_path.build_cip_path(path)?;
237
238 if path.len() % 2 != 0 {
240 path.push(0x00);
241 }
242
243 path.push(0x91);
245 path.push(member_name.len() as u8);
246 path.extend_from_slice(member_name.as_bytes());
247 }
248
249 TagPath::StringLength { base_path } => {
250 base_path.build_cip_path(path)?;
252
253 if path.len() % 2 != 0 {
255 path.push(0x00);
256 }
257
258 path.push(0x91);
260 path.push(3); path.extend_from_slice(b"LEN");
262 }
263
264 TagPath::StringData { base_path, index } => {
265 base_path.build_cip_path(path)?;
267
268 if path.len() % 2 != 0 {
270 path.push(0x00);
271 }
272
273 path.push(0x91);
275 path.push(4); path.extend_from_slice(b"DATA");
277
278 if path.len() % 2 != 0 {
280 path.push(0x00);
281 }
282
283 path.push(0x28); path.push(0x04); let index_u32 = *index;
287 path.extend_from_slice(&index_u32.to_le_bytes());
288 }
289 }
290
291 Ok(())
292 }
293
294 pub fn base_tag_name(&self) -> String {
296 match self {
297 TagPath::Controller { tag_name } => tag_name.clone(),
298 TagPath::Program { tag_name, .. } => tag_name.clone(),
299 TagPath::Array { base_path, .. } => base_path.base_tag_name(),
300 TagPath::Bit { base_path, .. } => base_path.base_tag_name(),
301 TagPath::Member { base_path, .. } => base_path.base_tag_name(),
302 TagPath::StringLength { base_path } => base_path.base_tag_name(),
303 TagPath::StringData { base_path, .. } => base_path.base_tag_name(),
304 }
305 }
306
307 pub fn is_program_scoped(&self) -> bool {
309 match self {
310 TagPath::Program { .. } => true,
311 TagPath::Array { base_path, .. } => base_path.is_program_scoped(),
312 TagPath::Bit { base_path, .. } => base_path.is_program_scoped(),
313 TagPath::Member { base_path, .. } => base_path.is_program_scoped(),
314 TagPath::StringLength { base_path } => base_path.is_program_scoped(),
315 TagPath::StringData { base_path, .. } => base_path.is_program_scoped(),
316 TagPath::Controller { .. } => false,
317 }
318 }
319
320 pub fn program_name(&self) -> Option<String> {
322 match self {
323 TagPath::Program { program_name, .. } => Some(program_name.clone()),
324 TagPath::Array { base_path, .. } => base_path.program_name(),
325 TagPath::Bit { base_path, .. } => base_path.program_name(),
326 TagPath::Member { base_path, .. } => base_path.program_name(),
327 TagPath::StringLength { base_path } => base_path.program_name(),
328 TagPath::StringData { base_path, .. } => base_path.program_name(),
329 TagPath::Controller { .. } => None,
330 }
331 }
332}
333
334impl fmt::Display for TagPath {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 write!(f, "{}", self.as_string())
337 }
338}
339
340struct TagPathParser<'a> {
342 input: &'a str,
343 position: usize,
344}
345
346impl<'a> TagPathParser<'a> {
347 fn new(input: &'a str) -> Self {
348 Self { input, position: 0 }
349 }
350
351 fn parse(mut self) -> Result<TagPath> {
352 self.parse_path()
353 }
354
355 fn parse_path(&mut self) -> Result<TagPath> {
356 if self.input.starts_with("Program:") {
358 self.parse_program_scoped()
359 } else {
360 self.parse_controller_scoped()
361 }
362 }
363
364 fn parse_program_scoped(&mut self) -> Result<TagPath> {
365 self.position = 8;
367
368 let program_name = self.parse_identifier()?;
370
371 if !self.consume_char('.') {
373 return Err(EtherNetIpError::Protocol(
374 "Expected '.' after program name".to_string(),
375 ));
376 }
377
378 let tag_name = self.parse_identifier()?;
380
381 let mut path = TagPath::Program {
382 program_name,
383 tag_name,
384 };
385
386 while self.position < self.input.len() {
388 path = self.parse_qualifier(path)?;
389 }
390
391 Ok(path)
392 }
393
394 fn parse_controller_scoped(&mut self) -> Result<TagPath> {
395 let tag_name = self.parse_identifier()?;
396 let mut path = TagPath::Controller { tag_name };
397
398 while self.position < self.input.len() {
400 path = self.parse_qualifier(path)?;
401 }
402
403 Ok(path)
404 }
405
406 fn parse_qualifier(&mut self, base_path: TagPath) -> Result<TagPath> {
407 match self.peek_char() {
408 Some('[') => self.parse_array_access(base_path),
409 Some('.') => self.parse_member_or_bit_access(base_path),
410 _ => Err(EtherNetIpError::Protocol(format!(
411 "Unexpected character at position {}",
412 self.position
413 ))),
414 }
415 }
416
417 fn parse_array_access(&mut self, base_path: TagPath) -> Result<TagPath> {
418 self.consume_char('[');
420
421 let mut indices = Vec::new();
422
423 indices.push(self.parse_number()?);
425
426 while self.peek_char() == Some(',') {
428 self.consume_char(',');
429 indices.push(self.parse_number()?);
430 }
431
432 if !self.consume_char(']') {
434 return Err(EtherNetIpError::Protocol(
435 "Expected ']' after array indices".to_string(),
436 ));
437 }
438
439 Ok(TagPath::Array {
440 base_path: Box::new(base_path),
441 indices,
442 })
443 }
444
445 fn parse_member_or_bit_access(&mut self, base_path: TagPath) -> Result<TagPath> {
446 self.consume_char('.');
448
449 if self.input[self.position..].starts_with("LEN") {
451 self.position += 3;
452 return Ok(TagPath::StringLength {
453 base_path: Box::new(base_path),
454 });
455 }
456
457 if self.input[self.position..].starts_with("DATA[") {
458 self.position += 5; let index = self.parse_number()?;
460 if !self.consume_char(']') {
461 return Err(EtherNetIpError::Protocol(
462 "Expected ']' after DATA index".to_string(),
463 ));
464 }
465 return Ok(TagPath::StringData {
466 base_path: Box::new(base_path),
467 index,
468 });
469 }
470
471 let identifier = self.parse_identifier()?;
473
474 if let Ok(bit_index) = identifier.parse::<u8>() {
476 if bit_index < 32 {
477 return Ok(TagPath::Bit {
479 base_path: Box::new(base_path),
480 bit_index,
481 });
482 }
483 }
484
485 Ok(TagPath::Member {
487 base_path: Box::new(base_path),
488 member_name: identifier,
489 })
490 }
491
492 fn parse_identifier(&mut self) -> Result<String> {
493 let start = self.position;
494
495 while self.position < self.input.len() {
496 let ch = self.input.chars().nth(self.position).unwrap();
497 if ch.is_alphanumeric() || ch == '_' {
498 self.position += 1;
499 } else {
500 break;
501 }
502 }
503
504 if start == self.position {
505 return Err(EtherNetIpError::Protocol("Expected identifier".to_string()));
506 }
507
508 Ok(self.input[start..self.position].to_string())
509 }
510
511 fn parse_number(&mut self) -> Result<u32> {
512 let start = self.position;
513
514 while self.position < self.input.len() {
515 let ch = self.input.chars().nth(self.position).unwrap();
516 if ch.is_ascii_digit() {
517 self.position += 1;
518 } else {
519 break;
520 }
521 }
522
523 if start == self.position {
524 return Err(EtherNetIpError::Protocol("Expected number".to_string()));
525 }
526
527 self.input[start..self.position]
528 .parse()
529 .map_err(|_| EtherNetIpError::Protocol("Invalid number".to_string()))
530 }
531
532 fn peek_char(&self) -> Option<char> {
533 self.input.chars().nth(self.position)
534 }
535
536 fn consume_char(&mut self, expected: char) -> bool {
537 if self.peek_char() == Some(expected) {
538 self.position += 1;
539 true
540 } else {
541 false
542 }
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn test_controller_scoped_tag() {
552 let path = TagPath::parse("MyTag").unwrap();
553 assert_eq!(
554 path,
555 TagPath::Controller {
556 tag_name: "MyTag".to_string()
557 }
558 );
559 assert_eq!(path.to_string(), "MyTag");
560 }
561
562 #[test]
563 fn test_program_scoped_tag() {
564 let path = TagPath::parse("Program:MainProgram.MyTag").unwrap();
565 assert_eq!(
566 path,
567 TagPath::Program {
568 program_name: "MainProgram".to_string(),
569 tag_name: "MyTag".to_string()
570 }
571 );
572 assert_eq!(path.to_string(), "Program:MainProgram.MyTag");
573 assert!(path.is_program_scoped());
574 assert_eq!(path.program_name(), Some("MainProgram".to_string()));
575 }
576
577 #[test]
578 fn test_array_access() {
579 let path = TagPath::parse("MyArray[5]").unwrap();
580 if let TagPath::Array { base_path, indices } = path {
581 assert_eq!(
582 *base_path,
583 TagPath::Controller {
584 tag_name: "MyArray".to_string()
585 }
586 );
587 assert_eq!(indices, vec![5]);
588 } else {
589 panic!("Expected Array path");
590 }
591 }
592
593 #[test]
594 fn test_multi_dimensional_array() {
595 let path = TagPath::parse("Matrix[1,2,3]").unwrap();
596 if let TagPath::Array { base_path, indices } = path {
597 assert_eq!(
598 *base_path,
599 TagPath::Controller {
600 tag_name: "Matrix".to_string()
601 }
602 );
603 assert_eq!(indices, vec![1, 2, 3]);
604 } else {
605 panic!("Expected Array path");
606 }
607 }
608
609 #[test]
610 fn test_bit_access() {
611 let path = TagPath::parse("StatusWord.15").unwrap();
612 if let TagPath::Bit {
613 base_path,
614 bit_index,
615 } = path
616 {
617 assert_eq!(
618 *base_path,
619 TagPath::Controller {
620 tag_name: "StatusWord".to_string()
621 }
622 );
623 assert_eq!(bit_index, 15);
624 } else {
625 panic!("Expected Bit path");
626 }
627 }
628
629 #[test]
630 fn test_member_access() {
631 let path = TagPath::parse("MotorData.Speed").unwrap();
632 if let TagPath::Member {
633 base_path,
634 member_name,
635 } = path
636 {
637 assert_eq!(
638 *base_path,
639 TagPath::Controller {
640 tag_name: "MotorData".to_string()
641 }
642 );
643 assert_eq!(member_name, "Speed");
644 } else {
645 panic!("Expected Member path");
646 }
647 }
648
649 #[test]
650 fn test_string_length() {
651 let path = TagPath::parse("MyString.LEN").unwrap();
652 if let TagPath::StringLength { base_path } = path {
653 assert_eq!(
654 *base_path,
655 TagPath::Controller {
656 tag_name: "MyString".to_string()
657 }
658 );
659 } else {
660 panic!("Expected StringLength path");
661 }
662 }
663
664 #[test]
665 fn test_string_data() {
666 let path = TagPath::parse("MyString.DATA[5]").unwrap();
667 if let TagPath::StringData { base_path, index } = path {
668 assert_eq!(
669 *base_path,
670 TagPath::Controller {
671 tag_name: "MyString".to_string()
672 }
673 );
674 assert_eq!(index, 5);
675 } else {
676 panic!("Expected StringData path");
677 }
678 }
679
680 #[test]
681 fn test_complex_nested_path() {
682 let path = TagPath::parse("Program:Safety.Devices[2].Status.15").unwrap();
683
684 if let TagPath::Bit {
687 base_path,
688 bit_index,
689 } = path
690 {
691 assert_eq!(bit_index, 15);
692
693 if let TagPath::Member {
694 base_path,
695 member_name,
696 } = *base_path
697 {
698 assert_eq!(member_name, "Status");
699
700 if let TagPath::Array { base_path, indices } = *base_path {
701 assert_eq!(indices, vec![2]);
702
703 if let TagPath::Program {
704 program_name,
705 tag_name,
706 } = *base_path
707 {
708 assert_eq!(program_name, "Safety");
709 assert_eq!(tag_name, "Devices");
710 } else {
711 panic!("Expected Program path");
712 }
713 } else {
714 panic!("Expected Array path");
715 }
716 } else {
717 panic!("Expected Member path");
718 }
719 } else {
720 panic!("Expected Bit path");
721 }
722 }
723
724 #[test]
725 fn test_cip_path_generation() {
726 let path = TagPath::parse("MyTag").unwrap();
727 let cip_path = path.to_cip_path().unwrap();
728
729 assert_eq!(cip_path[0], 0x91); assert_eq!(cip_path[1], 5); assert_eq!(&cip_path[2..7], b"MyTag");
733 assert_eq!(cip_path[7], 0x00); }
735
736 #[test]
737 fn test_array_cip_path_generation() {
738 let path = TagPath::parse("MyArray[5]").unwrap();
739 let cip_path = path.to_cip_path().unwrap();
740
741 assert_eq!(cip_path[0], 0x91); assert_eq!(cip_path[1], 7); assert_eq!(&cip_path[2..9], b"MyArray");
746 assert_eq!(cip_path[9], 0x00); assert_eq!(cip_path[10], 0x28); assert_eq!(cip_path[11], 0x05); assert_eq!(cip_path.len(), 12); }
754
755 #[test]
756 fn test_program_array_cip_path_generation() {
757 let path = TagPath::parse("Program:MainProgram.ArrayTest[0]").unwrap();
758 let cip_path = path.to_cip_path().unwrap();
759
760 tracing::debug!(
761 "Program array CIP path ({} bytes): {:02X?}",
762 cip_path.len(),
763 cip_path
764 );
765
766 assert_eq!(cip_path[0], 0x91);
769 assert_eq!(cip_path[1], 19); assert_eq!(&cip_path[2..21], b"Program:MainProgram");
771 assert_eq!(cip_path[21], 0x00); assert_eq!(cip_path[22], 0x91);
775 assert_eq!(cip_path[23], 9); assert_eq!(&cip_path[24..33], b"ArrayTest");
777 assert_eq!(cip_path[33], 0x00); assert_eq!(cip_path[34], 0x28); assert_eq!(cip_path[35], 0x00); assert_eq!(cip_path.len(), 36);
787 }
788
789 #[test]
790 fn test_base_tag_name() {
791 let path = TagPath::parse("Program:Main.MotorData[1].Speed.15").unwrap();
792 assert_eq!(path.base_tag_name(), "MotorData");
793 }
794
795 #[test]
796 fn test_invalid_paths() {
797 assert!(TagPath::parse("").is_err());
798 assert!(TagPath::parse("Program:").is_err());
799 assert!(TagPath::parse("MyArray[").is_err());
800 assert!(TagPath::parse("MyArray]").is_err());
801 assert!(TagPath::parse("MyTag.").is_err());
802 }
803}