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(|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 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 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 base_path.build_cip_path(path)?;
178
179 for &index in indices {
181 path.push(0x28); path.extend_from_slice(&index.to_le_bytes());
183 }
184 }
185
186 TagPath::Bit {
187 base_path,
188 bit_index,
189 } => {
190 base_path.build_cip_path(path)?;
192
193 path.push(0x29); path.push(*bit_index);
196 }
197
198 TagPath::Member {
199 base_path,
200 member_name,
201 } => {
202 base_path.build_cip_path(path)?;
204
205 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 base_path.build_cip_path(path)?;
214
215 path.push(0x91);
217 path.push(3); path.extend_from_slice(b"LEN");
219 }
220
221 TagPath::StringData { base_path, index } => {
222 base_path.build_cip_path(path)?;
224
225 path.push(0x91);
227 path.push(4); path.extend_from_slice(b"DATA");
229
230 path.push(0x28); path.extend_from_slice(&index.to_le_bytes());
233 }
234 }
235
236 Ok(())
237 }
238
239 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 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 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
285struct 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 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 self.position = 8;
312
313 let program_name = self.parse_identifier()?;
315
316 if !self.consume_char('.') {
318 return Err(EtherNetIpError::Protocol(
319 "Expected '.' after program name".to_string(),
320 ));
321 }
322
323 let tag_name = self.parse_identifier()?;
325
326 let mut path = TagPath::Program {
327 program_name,
328 tag_name,
329 };
330
331 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 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 self.consume_char('[');
365
366 let mut indices = Vec::new();
367
368 indices.push(self.parse_number()?);
370
371 while self.peek_char() == Some(',') {
373 self.consume_char(',');
374 indices.push(self.parse_number()?);
375 }
376
377 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 self.consume_char('.');
393
394 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; 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 let identifier = self.parse_identifier()?;
418
419 if let Ok(bit_index) = identifier.parse::<u8>() {
421 if bit_index < 32 {
422 return Ok(TagPath::Bit {
424 base_path: Box::new(base_path),
425 bit_index,
426 });
427 }
428 }
429
430 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 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 assert_eq!(cip_path[0], 0x91); assert_eq!(cip_path[1], 5); assert_eq!(&cip_path[2..7], b"MyTag");
678 assert_eq!(cip_path[7], 0x00); }
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}