1use std::fmt;
16use crate::error::{EtherNetIpError, Result};
17
18#[derive(Debug, Clone, PartialEq)]
20pub enum TagPath {
21 Controller {
23 tag_name: String,
24 },
25
26 Program {
28 program_name: String,
29 tag_name: String,
30 },
31
32 Array {
34 base_path: Box<TagPath>,
35 indices: Vec<u32>,
36 },
37
38 Bit {
40 base_path: Box<TagPath>,
41 bit_index: u8,
42 },
43
44 Member {
46 base_path: Box<TagPath>,
47 member_name: String,
48 },
49
50 StringLength {
52 base_path: Box<TagPath>,
53 },
54
55 StringData {
57 base_path: Box<TagPath>,
58 index: u32,
59 },
60}
61
62impl TagPath {
63 pub fn parse(path_str: &str) -> Result<Self> {
95 let parser = TagPathParser::new(path_str);
96 parser.parse()
97 }
98
99 pub fn as_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.as_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, bit_index)
116 }
117 TagPath::Member { base_path, member_name } => {
118 format!("{}.{}", base_path, member_name)
119 }
120 TagPath::StringLength { base_path } => {
121 format!("{}.LEN", base_path)
122 }
123 TagPath::StringData { base_path, index } => {
124 format!("{}.DATA[{}]", base_path, index)
125 }
126 }
127 }
128
129 pub fn to_cip_path(&self) -> Result<Vec<u8>> {
134 let mut path = Vec::new();
135 self.build_cip_path(&mut path)?;
136
137 if path.len() % 2 != 0 {
139 path.push(0x00);
140 }
141
142 Ok(path)
143 }
144
145 fn build_cip_path(&self, path: &mut Vec<u8>) -> Result<()> {
147 match self {
148 TagPath::Controller { tag_name } => {
149 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 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 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 base_path.build_cip_path(path)?;
172
173 for &index in indices {
175 path.push(0x28); path.extend_from_slice(&index.to_le_bytes());
177 }
178 }
179
180 TagPath::Bit { base_path, bit_index } => {
181 base_path.build_cip_path(path)?;
183
184 path.push(0x29); path.push(*bit_index);
187 }
188
189 TagPath::Member { base_path, member_name } => {
190 base_path.build_cip_path(path)?;
192
193 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 base_path.build_cip_path(path)?;
202
203 path.push(0x91);
205 path.push(3); path.extend_from_slice(b"LEN");
207 }
208
209 TagPath::StringData { base_path, index } => {
210 base_path.build_cip_path(path)?;
212
213 path.push(0x91);
215 path.push(4); path.extend_from_slice(b"DATA");
217
218 path.push(0x28); path.extend_from_slice(&index.to_le_bytes());
221 }
222 }
223
224 Ok(())
225 }
226
227 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 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 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.as_string())
270 }
271}
272
273struct 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 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 self.position = 8;
300
301 let program_name = self.parse_identifier()?;
303
304 if !self.consume_char('.') {
306 return Err(EtherNetIpError::Protocol(
307 "Expected '.' after program name".to_string()
308 ));
309 }
310
311 let tag_name = self.parse_identifier()?;
313
314 let mut path = TagPath::Program { program_name, tag_name };
315
316 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 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 self.consume_char('[');
349
350 let mut indices = Vec::new();
351
352 indices.push(self.parse_number()?);
354
355 while self.peek_char() == Some(',') {
357 self.consume_char(',');
358 indices.push(self.parse_number()?);
359 }
360
361 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 self.consume_char('.');
377
378 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; 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 let identifier = self.parse_identifier()?;
402
403 if let Ok(bit_index) = identifier.parse::<u8>() {
405 if bit_index < 32 { return Ok(TagPath::Bit {
407 base_path: Box::new(base_path),
408 bit_index,
409 });
410 }
411 }
412
413 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 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 assert_eq!(cip_path[0], 0x91); assert_eq!(cip_path[1], 5); assert_eq!(&cip_path[2..7], b"MyTag");
620 assert_eq!(cip_path[7], 0x00); }
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}