1use crate::EipClient;
2use crate::error::{EtherNetIpError, Result};
3use crate::udt::UdtDefinition;
4use std::collections::HashMap;
5use std::sync::{LazyLock, RwLock};
6use std::time::{Duration, Instant};
7use tracing;
8
9static TAG_NAME_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
10 regex::Regex::new(r"^(?:Program:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z_][A-Za-z0-9_]*(?:\[\d+\])?(?:\.[A-Za-z_][A-Za-z0-9_]*(?:\[\d+\])?)*$")
11 .expect("tag name regex pattern is a valid literal")
12});
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum TagScope {
17 Controller,
19 Program(String),
21 Global,
23 Local,
25}
26
27#[derive(Debug, Clone)]
29pub struct ArrayInfo {
30 pub dimensions: Vec<u32>,
32 pub element_count: u32,
34}
35
36#[derive(Debug, Clone)]
38pub struct TagMetadata {
39 pub data_type: u16,
41 pub size: u32,
43 pub is_array: bool,
45 pub dimensions: Vec<u32>,
47 pub permissions: TagPermissions,
49 pub scope: TagScope,
51 pub last_access: Instant,
53 pub array_info: Option<ArrayInfo>,
55 pub last_updated: Instant,
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub struct TagPermissions {
62 pub readable: bool,
64 pub writable: bool,
66}
67
68impl TagMetadata {
69 pub fn is_structure(&self) -> bool {
71 is_structure_type_word(self.data_type)
72 }
73}
74
75fn is_structure_type_word(data_type: u16) -> bool {
76 let type_code = data_type & 0x0fff;
77 (data_type & 0x8000) != 0 || (0x00A0..=0x00AF).contains(&type_code) || type_code == 0x02A0
78}
79
80#[derive(Debug)]
82#[deprecated(
83 since = "1.2.0",
84 note = "TagCache was never wired into live discovery; use TagManager's built-in cache instead. The type will be removed in 2.0."
85)]
86pub struct TagCache {
87 tags: HashMap<String, (TagMetadata, Instant)>,
89 expiration: Duration,
91}
92
93#[expect(
94 deprecated,
95 reason = "CODEX-AQ keeps the compatibility implementation until 2.0 removal"
96)]
97impl TagCache {
98 pub fn new(expiration: Duration) -> Self {
100 Self {
101 tags: HashMap::new(),
102 expiration,
103 }
104 }
105
106 pub fn update_tag(&mut self, name: String, metadata: TagMetadata) {
108 self.tags.insert(name, (metadata, Instant::now()));
109 }
110
111 pub fn get_tag(&self, name: &str) -> Option<&TagMetadata> {
113 if let Some((metadata, timestamp)) = self.tags.get(name)
114 && timestamp.elapsed() < self.expiration
115 {
116 return Some(metadata);
117 }
118 None
119 }
120
121 pub fn cleanup(&mut self) {
123 self.tags
124 .retain(|_, (_, timestamp)| timestamp.elapsed() < self.expiration);
125 }
126}
127
128#[derive(Debug)]
130pub struct TagManager {
131 pub cache: RwLock<HashMap<String, TagMetadata>>,
133 cache_duration: Duration,
134 pub udt_definitions: RwLock<HashMap<String, UdtDefinition>>,
136}
137
138impl TagManager {
139 pub fn new() -> Self {
141 Self {
142 cache: RwLock::new(HashMap::new()),
143 cache_duration: Duration::from_secs(300), udt_definitions: RwLock::new(HashMap::new()),
145 }
146 }
147
148 pub async fn get_metadata(&self, tag_name: &str) -> Result<Option<TagMetadata>> {
150 let cache = self.cache.read()?;
151 Ok(cache.get(tag_name).and_then(|metadata| {
152 if metadata.last_updated.elapsed() < self.cache_duration {
153 Some(metadata.clone())
154 } else {
155 None
156 }
157 }))
158 }
159
160 pub async fn update_metadata(&self, tag_name: String, metadata: TagMetadata) -> Result<()> {
162 self.cache.write()?.insert(tag_name, metadata);
163 Ok(())
164 }
165
166 pub async fn validate_tag(
168 &self,
169 tag_name: &str,
170 required_permissions: &TagPermissions,
171 ) -> Result<()> {
172 if let Some(metadata) = self.get_metadata(tag_name).await? {
173 if !metadata.permissions.readable && required_permissions.readable {
174 return Err(EtherNetIpError::Permission(format!(
175 "Tag '{tag_name}' is not readable"
176 )));
177 }
178 if !metadata.permissions.writable && required_permissions.writable {
179 return Err(EtherNetIpError::Permission(format!(
180 "Tag '{tag_name}' is not writable"
181 )));
182 }
183 Ok(())
184 } else {
185 Err(EtherNetIpError::Tag(format!("Tag '{tag_name}' not found")))
186 }
187 }
188
189 pub async fn clear_cache(&self) -> Result<()> {
191 self.cache.write()?.clear();
192 Ok(())
193 }
194
195 pub async fn remove_stale_entries(&self) -> Result<()> {
197 self.cache
198 .write()?
199 .retain(|_, metadata| metadata.last_updated.elapsed() < self.cache_duration);
200 Ok(())
201 }
202
203 pub async fn discover_tags(&self, client: &mut EipClient) -> Result<()> {
205 let response = client
206 .send_cip_request(&client.build_list_tags_request())
207 .await?;
208 let tags = self.parse_tag_list(&response)?;
209
210 let mut all_tags = Vec::new();
212 for (name, metadata) in tags {
213 all_tags.push((name, metadata));
214 }
215
216 let hierarchical_tags = self.discover_hierarchical_tags(client, &all_tags).await?;
218
219 let mut cache = self.cache.write()?;
220 for (name, metadata) in hierarchical_tags {
221 cache.insert(name, metadata);
222 }
223 Ok(())
224 }
225
226 async fn discover_hierarchical_tags(
228 &self,
229 client: &mut EipClient,
230 base_tags: &[(String, TagMetadata)],
231 ) -> Result<Vec<(String, TagMetadata)>> {
232 let mut all_tags = Vec::new();
233 let mut tag_names = std::collections::HashSet::new();
234
235 for (name, metadata) in base_tags {
237 if self.validate_tag_name(name) {
238 all_tags.push((name.clone(), metadata.clone()));
239 tag_names.insert(name.clone());
240 }
241 }
242
243 for (name, metadata) in base_tags {
245 if metadata.is_structure() && !metadata.is_array {
246 if let Ok(members) = self.discover_udt_members(client, name).await {
248 for (member_name, member_metadata) in members {
249 let full_name = format!("{}.{}", name, member_name);
250 if self.validate_tag_name(&full_name) && !tag_names.contains(&full_name) {
251 all_tags.push((full_name.clone(), member_metadata.clone()));
252 tag_names.insert(full_name.clone());
253
254 if member_metadata.is_structure()
256 && !member_metadata.is_array
257 && let Ok(nested_members) =
258 self.discover_udt_members(client, &full_name).await
259 {
260 for (nested_name, nested_metadata) in nested_members {
261 let nested_full_name = format!("{}.{}", full_name, nested_name);
262 if self.validate_tag_name(&nested_full_name)
263 && !tag_names.contains(&nested_full_name)
264 {
265 all_tags.push((nested_full_name.clone(), nested_metadata));
266 tag_names.insert(nested_full_name);
267 }
268 }
269 }
270 }
271 }
272 }
273 }
274 }
275
276 tracing::debug!(
277 "Discovered {} total tags (including hierarchical)",
278 all_tags.len()
279 );
280 Ok(all_tags)
281 }
282
283 pub async fn discover_udt_members(
285 &self,
286 client: &mut EipClient,
287 udt_name: &str,
288 ) -> Result<Vec<(String, TagMetadata)>> {
289 tracing::debug!("Discovering UDT members for: {}", udt_name);
290
291 let udt_definition = client.get_udt_definition(udt_name).await?;
292
293 {
294 let mut definitions = self.udt_definitions.write()?;
295 definitions.insert(udt_name.to_string(), udt_definition.clone());
296 }
297
298 let mut members = Vec::new();
299 for member in &udt_definition.members {
300 let full_name = format!("{}.{}", udt_name, member.name);
301 if !self.validate_tag_name(&full_name) {
302 tracing::warn!("Skipping invalid UDT member path: {}", full_name);
303 continue;
304 }
305
306 let metadata = TagMetadata {
307 data_type: member.data_type,
308 scope: TagScope::Controller,
309 permissions: TagPermissions {
310 readable: true,
311 writable: true,
312 },
313 is_array: false,
314 dimensions: Vec::new(),
315 last_access: Instant::now(),
316 size: member.size,
317 array_info: None,
318 last_updated: Instant::now(),
319 };
320
321 tracing::trace!(
322 "Found UDT member: {} (Type: 0x{:04X})",
323 full_name,
324 member.data_type
325 );
326 members.push((member.name.clone(), metadata));
327 }
328
329 Ok(members)
330 }
331
332 #[deprecated(
334 since = "1.2.0",
335 note = "This method fabricated an invalid UDT request. Use EipClient::get_udt_definition or EipClient::discover_udt_members instead. It will be removed in 2.0."
336 )]
337 pub fn build_udt_definition_request(&self, _udt_name: &str) -> Result<Vec<u8>> {
338 Err(EtherNetIpError::Unsupported {
339 api: "TagManager::build_udt_definition_request",
340 reason: "the old request builder emitted an invalid Read Tag shape; use EipClient::get_udt_definition or EipClient::discover_udt_members",
341 })
342 }
343
344 #[deprecated(
346 since = "1.2.0",
347 note = "This method fabricated UDT members from arbitrary bytes. Use EipClient::get_udt_definition or EipClient::discover_udt_members instead. It will be removed in 2.0."
348 )]
349 pub fn parse_udt_definition_response(
350 &self,
351 _response: &[u8],
352 _udt_name: &str,
353 ) -> Result<UdtDefinition> {
354 Err(EtherNetIpError::Unsupported {
355 api: "TagManager::parse_udt_definition_response",
356 reason: "the old parser invented member names and fallback fields; use EipClient::get_udt_definition or EipClient::discover_udt_members",
357 })
358 }
359
360 fn validate_tag_name(&self, tag_name: &str) -> bool {
362 if tag_name.is_empty() || tag_name.trim().is_empty() {
363 return false;
364 }
365
366 if !TAG_NAME_RE.is_match(tag_name) {
368 return false;
369 }
370
371 if tag_name.starts_with(char::is_numeric) {
373 return false;
374 }
375
376 if tag_name.contains("__") || tag_name.contains("..") {
377 return false;
378 }
379
380 true
381 }
382
383 pub fn get_udt_definition_cached(&self, udt_name: &str) -> Option<UdtDefinition> {
385 self.udt_definitions
386 .read()
387 .ok()
388 .and_then(|definitions| definitions.get(udt_name).cloned())
389 }
390
391 pub fn list_udt_definitions(&self) -> Vec<String> {
393 self.udt_definitions
394 .read()
395 .map(|definitions| definitions.keys().cloned().collect())
396 .unwrap_or_default()
397 }
398
399 pub fn clear_udt_cache(&self) {
401 if let Ok(mut definitions) = self.udt_definitions.write() {
402 definitions.clear();
403 }
404 }
405
406 pub fn parse_tag_list(&self, response: &[u8]) -> Result<Vec<(String, TagMetadata)>> {
408 tracing::trace!(
409 "Raw tag list response ({} bytes): {:02X?}",
410 response.len(),
411 response
412 );
413
414 if response.len() >= 3 {
416 let service_reply = response[0];
417 let general_status = response[2];
418
419 if general_status != 0x00 {
421 let error_msg = match general_status {
423 0x01 => "Connection failure - Tag discovery may not be supported on this PLC",
424 0x04 => "Path segment error",
425 0x05 => "Path destination unknown",
426 0x16 => "Object does not exist",
427 _ => "Unknown CIP error",
428 };
429 return Err(crate::error::EtherNetIpError::Protocol(format!(
430 "CIP Error 0x{:02X} during tag discovery: {}. Some PLCs do not support tag discovery. Try reading tags directly by name.",
431 general_status, error_msg
432 )));
433 }
434
435 if service_reply != 0xD5 && service_reply != 0x55 {
437 if general_status == 0x00 {
439 tracing::warn!(
440 "Unexpected service reply 0x{:02X}, but status is 0x00, attempting to parse",
441 service_reply
442 );
443 }
444 }
445 }
446
447 let mut tags = Vec::new();
448
449 if response.len() < 8 {
454 return Err(crate::error::EtherNetIpError::Protocol(
455 "Response too short for tag list".to_string(),
456 ));
457 }
458
459 let additional_status_bytes = response[3] as usize * 2;
463 let item_count_at = 4 + additional_status_bytes;
464 if response.len() < item_count_at + 4 {
465 return Err(crate::error::EtherNetIpError::Protocol(
466 "Response too short for tag list item count".to_string(),
467 ));
468 }
469 let item_count = u32::from_le_bytes([
470 response[item_count_at],
471 response[item_count_at + 1],
472 response[item_count_at + 2],
473 response[item_count_at + 3],
474 ]);
475 tracing::debug!("Detected item count: {}", item_count);
476
477 let mut offset = item_count_at + 4;
479
480 for item_index in 0..item_count {
483 if offset + 4 > response.len() {
485 return Err(crate::error::EtherNetIpError::Protocol(format!(
486 "Tag list ended before instance ID for item {item_index} at offset {offset}"
487 )));
488 }
489
490 let instance_id = u32::from_le_bytes([
491 response[offset],
492 response[offset + 1],
493 response[offset + 2],
494 response[offset + 3],
495 ]);
496 offset += 4;
497
498 if offset + 2 > response.len() {
500 return Err(crate::error::EtherNetIpError::Protocol(format!(
501 "Tag list ended before name length for item {item_index} at offset {offset}"
502 )));
503 }
504
505 let name_length = u16::from_le_bytes([response[offset], response[offset + 1]]) as usize;
506 offset += 2;
507
508 if name_length > 1000 || name_length == 0 {
509 return Err(crate::error::EtherNetIpError::Protocol(format!(
510 "Invalid tag-list name length {} for item {} at offset {}",
511 name_length,
512 item_index,
513 offset - 2
514 )));
515 }
516
517 if offset
519 .checked_add(name_length)
520 .is_none_or(|end| end > response.len())
521 {
522 return Err(crate::error::EtherNetIpError::Protocol(format!(
523 "Not enough bytes for tag name at offset {} (need {}, have {})",
524 offset,
525 name_length,
526 response.len() - offset
527 )));
528 }
529
530 let name = String::from_utf8_lossy(&response[offset..offset + name_length]).to_string();
531 offset += name_length;
532
533 if offset + 2 > response.len() {
535 return Err(crate::error::EtherNetIpError::Protocol(format!(
536 "Tag list ended before type word for item {item_index} at offset {offset}"
537 )));
538 }
539
540 let tag_type = u16::from_le_bytes([response[offset], response[offset + 1]]);
541 offset += 2;
542
543 let (type_code, is_structure, array_dims, _reserved) = self.parse_tag_type(tag_type);
545
546 let is_array = array_dims > 0;
547 let dimensions = Vec::new();
548 let array_info = None;
549
550 if !self.is_valid_tag_type(type_code) {
552 tracing::debug!(
553 "Skipping tag {} - unsupported type 0x{:04X}",
554 name,
555 type_code
556 );
557 continue;
558 }
559
560 let metadata = TagMetadata {
561 data_type: type_code,
562 scope: TagScope::Controller,
563 permissions: TagPermissions {
564 readable: true,
565 writable: true,
566 },
567 is_array,
568 dimensions,
569 last_access: Instant::now(),
570 size: 0,
571 array_info,
572 last_updated: Instant::now(),
573 };
574
575 tracing::trace!(
576 "Parsed tag: {} (ID: {}, Type: 0x{:04X}, Structure: {})",
577 name,
578 instance_id,
579 type_code,
580 is_structure
581 );
582
583 tags.push((name, metadata));
584 }
585
586 tracing::debug!("Parsed {} tags from response", tags.len());
587 Ok(tags)
588 }
589
590 fn parse_tag_type(&self, tag_type: u16) -> (u16, bool, u8, bool) {
592 let type_code = if (tag_type & 0x00ff) == 0xc1 {
593 0x00c1
594 } else {
595 tag_type & 0x0fff
596 };
597
598 let is_structure = is_structure_type_word(tag_type) || is_structure_type_word(type_code);
599 let array_dims = ((tag_type & 0x6000) >> 13) as u8;
600 let reserved = (tag_type & 0x1000) != 0;
601
602 (type_code, is_structure, array_dims, reserved)
603 }
604
605 fn is_valid_tag_type(&self, type_code: u16) -> bool {
607 match type_code {
608 0x00C1 => true, 0x00C2 => true, 0x00C3 => true, 0x00C4 => true, 0x00C5 => true, 0x00C6 => true, 0x00C7 => true, 0x00C8 => true, 0x00C9 => true, 0x00CA => true, 0x00CB => true, 0x00CE => true, _ => is_structure_type_word(type_code),
621 }
622 }
623
624 pub async fn drill_down_tags(
626 &self,
627 base_tags: &[(String, TagMetadata)],
628 ) -> Result<Vec<(String, TagMetadata)>> {
629 let mut all_tags = Vec::new();
630 let mut tag_names = std::collections::HashSet::new();
631
632 for (tag_name, metadata) in base_tags {
634 self.drill_down_recursive(&mut all_tags, &mut tag_names, tag_name, metadata, "")?;
635 }
636
637 tracing::debug!(
638 "Drill down completed: {} total tags discovered",
639 all_tags.len()
640 );
641 Ok(all_tags)
642 }
643
644 fn drill_down_recursive(
646 &self,
647 all_tags: &mut Vec<(String, TagMetadata)>,
648 tag_names: &mut std::collections::HashSet<String>,
649 tag_name: &str,
650 metadata: &TagMetadata,
651 previous_name: &str,
652 ) -> Result<()> {
653 if metadata.is_array {
655 return Ok(());
656 }
657
658 let new_name = if previous_name.is_empty() {
659 tag_name.to_string()
660 } else {
661 format!("{}.{}", previous_name, tag_name)
662 };
663
664 if metadata.is_structure() && !metadata.is_array {
666 if self.validate_tag_name(&new_name) && !tag_names.contains(&new_name) {
669 all_tags.push((new_name.clone(), metadata.clone()));
670 tag_names.insert(new_name);
671 }
672 } else {
673 if self.is_valid_tag_type(metadata.data_type)
675 && self.validate_tag_name(&new_name)
676 && !tag_names.contains(&new_name)
677 {
678 all_tags.push((new_name.clone(), metadata.clone()));
679 tag_names.insert(new_name);
680 }
681 }
682
683 Ok(())
684 }
685}
686
687impl Default for TagManager {
688 fn default() -> Self {
689 Self::new()
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696 use crate::udt::UdtMember;
697
698 #[test]
699 #[expect(
700 deprecated,
701 reason = "CODEX-AQ keeps TagCache covered until 2.0 removal"
702 )]
703 fn test_tag_cache_expiration() {
704 let mut cache = TagCache::new(Duration::from_secs(1));
705 let metadata = TagMetadata {
706 data_type: 0x00C1,
707 size: 1,
708 is_array: false,
709 dimensions: vec![],
710 permissions: TagPermissions {
711 readable: true,
712 writable: true,
713 },
714 scope: TagScope::Controller,
715 last_access: Instant::now(),
716 array_info: None,
717 last_updated: Instant::now(),
718 };
719
720 cache.update_tag("TestTag".to_string(), metadata);
721 assert!(cache.get_tag("TestTag").is_some());
722
723 std::thread::sleep(Duration::from_secs(2));
725 assert!(cache.get_tag("TestTag").is_none());
726 }
727
728 #[test]
729 fn test_tag_metadata_is_structure() {
730 let bool_metadata = TagMetadata {
732 data_type: 0x00C1,
733 size: 1,
734 is_array: false,
735 dimensions: vec![],
736 permissions: TagPermissions {
737 readable: true,
738 writable: true,
739 },
740 scope: TagScope::Controller,
741 last_access: Instant::now(),
742 array_info: None,
743 last_updated: Instant::now(),
744 };
745 assert!(!bool_metadata.is_structure());
746
747 let dint_metadata = TagMetadata {
749 data_type: 0x00C4,
750 size: 4,
751 is_array: false,
752 dimensions: vec![],
753 permissions: TagPermissions {
754 readable: true,
755 writable: true,
756 },
757 scope: TagScope::Controller,
758 last_access: Instant::now(),
759 array_info: None,
760 last_updated: Instant::now(),
761 };
762 assert!(!dint_metadata.is_structure());
763
764 let udt_metadata = TagMetadata {
766 data_type: 0x00A0,
767 size: 20,
768 is_array: false,
769 dimensions: vec![],
770 permissions: TagPermissions {
771 readable: true,
772 writable: true,
773 },
774 scope: TagScope::Controller,
775 last_access: Instant::now(),
776 array_info: None,
777 last_updated: Instant::now(),
778 };
779 assert!(udt_metadata.is_structure());
780 }
781
782 #[test]
783 fn test_validate_tag_name() {
784 let tag_manager = TagManager::new();
785
786 assert!(tag_manager.validate_tag_name("ValidTag"));
788 assert!(tag_manager.validate_tag_name("Valid_Tag"));
789 assert!(tag_manager.validate_tag_name("Valid.Tag"));
790 assert!(tag_manager.validate_tag_name("Valid123"));
791 assert!(tag_manager.validate_tag_name("Valid_Tag123"));
792 assert!(tag_manager.validate_tag_name("Valid.Tag123"));
793 assert!(tag_manager.validate_tag_name("_Tag"));
794 assert!(tag_manager.validate_tag_name("Program:Main.Tag"));
795 assert!(tag_manager.validate_tag_name("Arr[3]"));
796 assert!(tag_manager.validate_tag_name("Program:Main.Arr[3].Member"));
797
798 assert!(!tag_manager.validate_tag_name("")); assert!(!tag_manager.validate_tag_name(" ")); assert!(!tag_manager.validate_tag_name("123Invalid")); assert!(!tag_manager.validate_tag_name("Invalid..Tag")); assert!(!tag_manager.validate_tag_name("Invalid-Tag")); assert!(!tag_manager.validate_tag_name("Invalid Tag")); assert!(!tag_manager.validate_tag_name("Invalid@Tag")); }
807
808 #[test]
809 fn test_parse_tag_type() {
810 let tag_manager = TagManager::new();
811
812 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x00C1);
814 assert_eq!(type_code, 0x00C1);
815 assert!(!is_structure);
816 assert_eq!(array_dims, 0);
817 assert!(!reserved);
818
819 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x00C4);
821 assert_eq!(type_code, 0x00C4);
822 assert!(!is_structure);
823 assert_eq!(array_dims, 0);
824 assert!(!reserved);
825
826 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x80A0);
828 assert_eq!(type_code, 0x00A0);
829 assert!(is_structure);
830 assert_eq!(array_dims, 0);
831 assert!(!reserved);
832
833 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x82A0);
834 assert_eq!(type_code, 0x02A0);
835 assert!(is_structure);
836 assert_eq!(array_dims, 0);
837 assert!(!reserved);
838
839 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x20C4);
841 assert_eq!(type_code, 0x00C4);
842 assert!(!is_structure);
843 assert_eq!(array_dims, 1);
844 assert!(!reserved);
845
846 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x40C4);
848 assert_eq!(type_code, 0x00C4);
849 assert!(!is_structure);
850 assert_eq!(array_dims, 2);
851 assert!(!reserved);
852 }
853
854 #[test]
855 fn test_udt_definition_caching() {
856 let tag_manager = TagManager::new();
857
858 assert!(tag_manager.list_udt_definitions().is_empty());
860
861 let udt_def = UdtDefinition {
863 name: "TestUDT".to_string(),
864 members: vec![
865 UdtMember {
866 name: "Value1".to_string(),
867 data_type: 0x00C1,
868 offset: 0,
869 size: 1,
870 },
871 UdtMember {
872 name: "Value2".to_string(),
873 data_type: 0x00C4,
874 offset: 4,
875 size: 4,
876 },
877 ],
878 };
879
880 {
882 let mut definitions = tag_manager
883 .udt_definitions
884 .write()
885 .expect("test UDT definition cache lock should not be poisoned");
886 definitions.insert("TestUDT".to_string(), udt_def);
887 }
888
889 let retrieved = tag_manager.get_udt_definition_cached("TestUDT");
891 assert!(retrieved.is_some());
892 let retrieved = retrieved.unwrap();
893 assert_eq!(retrieved.name, "TestUDT");
894 assert_eq!(retrieved.members.len(), 2);
895
896 let udt_list = tag_manager.list_udt_definitions();
898 assert_eq!(udt_list.len(), 1);
899 assert_eq!(udt_list[0], "TestUDT");
900
901 tag_manager.clear_udt_cache();
903 assert!(tag_manager.list_udt_definitions().is_empty());
904 assert!(tag_manager.get_udt_definition_cached("TestUDT").is_none());
905 }
906
907 #[test]
908 fn test_parse_tag_list_with_invalid_data() {
909 let tag_manager = TagManager::new();
910
911 let invalid_response = [
913 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, ];
917
918 let result = tag_manager.parse_tag_list(&invalid_response);
919 assert!(result.is_err());
920 }
921
922 #[test]
923 fn test_parse_tag_list_with_valid_data() {
924 let tag_manager = TagManager::new();
925
926 let valid_response = [
927 0xD5, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x09, 0x00, b'M', b'o', b't', b'o', b'r', b'D', b'a', b't', b'a', 0xC4, 0x00, ];
934
935 let tags = tag_manager.parse_tag_list(&valid_response).unwrap();
936 assert_eq!(tags.len(), 1);
937 assert_eq!(tags[0].0, "MotorData");
938 assert_eq!(tags[0].1.data_type, 0x00C4);
939 assert!(!tags[0].1.is_array);
940 assert!(tags[0].1.dimensions.is_empty());
941 assert!(tags[0].1.array_info.is_none());
942 }
943
944 #[test]
945 fn test_tag_scope_enum() {
946 let controller_scope = TagScope::Controller;
948 assert_eq!(controller_scope, TagScope::Controller);
949
950 let program_scope = TagScope::Program("MainProgram".to_string());
952 match program_scope {
953 TagScope::Program(name) => assert_eq!(name, "MainProgram"),
954 _ => panic!("Expected Program scope"),
955 }
956
957 let global_scope = TagScope::Global;
959 assert_eq!(global_scope, TagScope::Global);
960
961 let local_scope = TagScope::Local;
963 assert_eq!(local_scope, TagScope::Local);
964 }
965
966 #[test]
967 fn test_array_info() {
968 let array_info = ArrayInfo {
969 dimensions: vec![10, 20],
970 element_count: 200,
971 };
972
973 assert_eq!(array_info.dimensions, vec![10, 20]);
974 assert_eq!(array_info.element_count, 200);
975 }
976
977 #[test]
978 fn test_tag_permissions() {
979 let permissions = TagPermissions {
980 readable: true,
981 writable: false,
982 };
983
984 assert!(permissions.readable);
985 assert!(!permissions.writable);
986 }
987}