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