1use std::sync::LazyLock;
11
12use regex::Regex;
13use thiserror::Error;
14use tracing::error;
15
16use crate::{
17 node_id::{Identifier, NodeId},
18 qualified_name::QualifiedName,
19 string::UAString,
20 ReferenceTypeId, RelativePath, RelativePathElement,
21};
22
23impl RelativePath {
24 const MAX_TOKEN_LEN: usize = 256;
26 const MAX_ELEMENTS: usize = 32;
28
29 pub fn from_str<CB>(path: &str, node_resolver: &CB) -> Result<RelativePath, RelativePathError>
33 where
34 CB: Fn(u16, &str) -> Option<NodeId>,
35 {
36 let mut elements: Vec<RelativePathElement> = Vec::new();
37
38 let mut escaped_char = false;
42 let mut token = String::with_capacity(path.len());
43 for c in path.chars() {
44 if escaped_char {
45 token.push(c);
46 escaped_char = false;
47 } else {
48 match c {
50 '&' => {
51 escaped_char = true;
53 }
54 '/' | '.' | '<' if !token.is_empty() => {
56 if elements.len() == Self::MAX_ELEMENTS {
57 break;
58 }
59 elements.push(RelativePathElement::from_str(&token, node_resolver)?);
60 token.clear();
61 }
62 _ => {}
63 }
64 token.push(c);
65 }
66 if token.len() > Self::MAX_TOKEN_LEN {
67 error!("Path segment seems unusually long and has been rejected");
68 return Err(RelativePathError::PathSegmentTooLong);
69 }
70 }
71
72 if !token.is_empty() {
73 if elements.len() == Self::MAX_ELEMENTS {
74 error!("Number of elements in relative path is too long, rejecting it");
75 return Err(RelativePathError::TooManyElements);
76 }
77 elements.push(RelativePathElement::from_str(&token, node_resolver)?);
78 }
79
80 Ok(RelativePath {
81 elements: Some(elements),
82 })
83 }
84}
85
86impl From<&[QualifiedName]> for RelativePath {
87 fn from(value: &[QualifiedName]) -> Self {
88 let elements = value
89 .iter()
90 .map(|qn| RelativePathElement {
91 reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
92 is_inverse: false,
93 include_subtypes: true,
94 target_name: qn.clone(),
95 })
96 .collect();
97 Self {
98 elements: Some(elements),
99 }
100 }
101}
102impl TryFrom<&str> for RelativePath {
107 type Error = RelativePathError;
108
109 fn try_from(value: &str) -> Result<Self, Self::Error> {
110 RelativePath::from_str(value, &RelativePathElement::default_node_resolver)
111 }
112}
113
114impl TryFrom<&String> for RelativePath {
115 type Error = RelativePathError;
116
117 fn try_from(value: &String) -> Result<Self, Self::Error> {
118 RelativePath::from_str(value, &RelativePathElement::default_node_resolver)
119 }
120}
121
122impl TryFrom<String> for RelativePath {
123 type Error = RelativePathError;
124
125 fn try_from(value: String) -> Result<Self, Self::Error> {
126 RelativePath::from_str(&value, &RelativePathElement::default_node_resolver)
127 }
128}
129impl<'a> From<&'a RelativePathElement> for String {
130 fn from(element: &'a RelativePathElement) -> String {
131 let mut result = element
132 .relative_path_reference_type(&RelativePathElement::default_browse_name_resolver);
133 if !element.target_name.name.is_null() {
134 let always_use_namespace = true;
135 let target_browse_name = escape_browse_name(element.target_name.name.as_ref());
136 if always_use_namespace || element.target_name.namespace_index > 0 {
137 result.push_str(&format!(
138 "{}:{}",
139 element.target_name.namespace_index, target_browse_name
140 ));
141 } else {
142 result.push_str(&target_browse_name);
143 }
144 }
145 result
146 }
147}
148
149impl RelativePathElement {
150 pub fn default_node_resolver(namespace: u16, browse_name: &str) -> Option<NodeId> {
157 let node_id = if namespace == 0 {
158 match browse_name {
159 "References" => ReferenceTypeId::References.into(),
160 "NonHierarchicalReferences" => ReferenceTypeId::NonHierarchicalReferences.into(),
161 "HierarchicalReferences" => ReferenceTypeId::HierarchicalReferences.into(),
162 "HasChild" => ReferenceTypeId::HasChild.into(),
163 "Organizes" => ReferenceTypeId::Organizes.into(),
164 "HasEventSource" => ReferenceTypeId::HasEventSource.into(),
165 "HasModellingRule" => ReferenceTypeId::HasModellingRule.into(),
166 "HasEncoding" => ReferenceTypeId::HasEncoding.into(),
167 "HasDescription" => ReferenceTypeId::HasDescription.into(),
168 "HasTypeDefinition" => ReferenceTypeId::HasTypeDefinition.into(),
169 "GeneratesEvent" => ReferenceTypeId::GeneratesEvent.into(),
170 "Aggregates" => ReferenceTypeId::Aggregates.into(),
171 "HasSubtype" => ReferenceTypeId::HasSubtype.into(),
172 "HasProperty" => ReferenceTypeId::HasProperty.into(),
173 "HasComponent" => ReferenceTypeId::HasComponent.into(),
174 "HasNotifier" => ReferenceTypeId::HasNotifier.into(),
175 "HasOrderedComponent" => ReferenceTypeId::HasOrderedComponent.into(),
176 "FromState" => ReferenceTypeId::FromState.into(),
177 "ToState" => ReferenceTypeId::ToState.into(),
178 "HasCause" => ReferenceTypeId::HasCause.into(),
179 "HasEffect" => ReferenceTypeId::HasEffect.into(),
180 "HasHistoricalConfiguration" => ReferenceTypeId::HasHistoricalConfiguration.into(),
181 "HasSubStateMachine" => ReferenceTypeId::HasSubStateMachine.into(),
182 "AlwaysGeneratesEvent" => ReferenceTypeId::AlwaysGeneratesEvent.into(),
183 "HasTrueSubState" => ReferenceTypeId::HasTrueSubState.into(),
184 "HasFalseSubState" => ReferenceTypeId::HasFalseSubState.into(),
185 "HasCondition" => ReferenceTypeId::HasCondition.into(),
186 _ => NodeId::new(0, UAString::from(browse_name)),
187 }
188 } else {
189 NodeId::new(namespace, UAString::from(browse_name))
190 };
191 Some(node_id)
192 }
193
194 fn id_from_reference_type(id: u32) -> Option<String> {
195 Some(
197 match id {
198 id if id == ReferenceTypeId::References as u32 => "References",
199 id if id == ReferenceTypeId::NonHierarchicalReferences as u32 => {
200 "NonHierarchicalReferences"
201 }
202 id if id == ReferenceTypeId::HierarchicalReferences as u32 => {
203 "HierarchicalReferences"
204 }
205 id if id == ReferenceTypeId::HasChild as u32 => "HasChild",
206 id if id == ReferenceTypeId::Organizes as u32 => "Organizes",
207 id if id == ReferenceTypeId::HasEventSource as u32 => "HasEventSource",
208 id if id == ReferenceTypeId::HasModellingRule as u32 => "HasModellingRule",
209 id if id == ReferenceTypeId::HasEncoding as u32 => "HasEncoding",
210 id if id == ReferenceTypeId::HasDescription as u32 => "HasDescription",
211 id if id == ReferenceTypeId::HasTypeDefinition as u32 => "HasTypeDefinition",
212 id if id == ReferenceTypeId::GeneratesEvent as u32 => "GeneratesEvent",
213 id if id == ReferenceTypeId::Aggregates as u32 => "Aggregates",
214 id if id == ReferenceTypeId::HasSubtype as u32 => "HasSubtype",
215 id if id == ReferenceTypeId::HasProperty as u32 => "HasProperty",
216 id if id == ReferenceTypeId::HasComponent as u32 => "HasComponent",
217 id if id == ReferenceTypeId::HasNotifier as u32 => "HasNotifier",
218 id if id == ReferenceTypeId::HasOrderedComponent as u32 => "HasOrderedComponent",
219 id if id == ReferenceTypeId::FromState as u32 => "FromState",
220 id if id == ReferenceTypeId::ToState as u32 => "ToState",
221 id if id == ReferenceTypeId::HasCause as u32 => "HasCause",
222 id if id == ReferenceTypeId::HasEffect as u32 => "HasEffect",
223 id if id == ReferenceTypeId::HasHistoricalConfiguration as u32 => {
224 "HasHistoricalConfiguration"
225 }
226 id if id == ReferenceTypeId::HasSubStateMachine as u32 => "HasSubStateMachine",
227 id if id == ReferenceTypeId::AlwaysGeneratesEvent as u32 => "AlwaysGeneratesEvent",
228 id if id == ReferenceTypeId::HasTrueSubState as u32 => "HasTrueSubState",
229 id if id == ReferenceTypeId::HasFalseSubState as u32 => "HasFalseSubState",
230 id if id == ReferenceTypeId::HasCondition as u32 => "HasCondition",
231 _ => return None,
232 }
233 .to_string(),
234 )
235 }
236
237 fn default_browse_name_resolver(node_id: &NodeId) -> Option<String> {
238 match &node_id.identifier {
239 Identifier::String(browse_name) => Some(browse_name.as_ref().to_string()),
240 Identifier::Numeric(id) => {
241 if node_id.namespace == 0 {
242 Self::id_from_reference_type(*id)
243 } else {
244 None
245 }
246 }
247 _ => None,
248 }
249 }
250
251 pub fn from_str<CB>(
272 path: &str,
273 node_resolver: &CB,
274 ) -> Result<RelativePathElement, RelativePathError>
275 where
276 CB: Fn(u16, &str) -> Option<NodeId>,
277 {
278 static RE: LazyLock<Regex> = LazyLock::new(|| {
279 Regex::new(r"(?P<reftype>/|\.|(<(?P<flags>#|!|#!)?((?P<nsidx>[0-9]+):)?(?P<name>[^#!].*)>))(?P<target>.*)").unwrap()
280 });
281
282 if let Some(captures) = RE.captures(path) {
285 let target_name = target_name(captures.name("target").unwrap().as_str())?;
286
287 let reference_type = captures.name("reftype").unwrap();
288 let (reference_type_id, include_subtypes, is_inverse) = match reference_type.as_str() {
289 "/" => (ReferenceTypeId::HierarchicalReferences.into(), true, false),
290 "." => (ReferenceTypeId::Aggregates.into(), true, false),
291 _ => {
292 let (include_subtypes, is_inverse) = if let Some(flags) = captures.name("flags")
293 {
294 match flags.as_str() {
295 "#" => (false, false),
296 "!" => (true, true),
297 "#!" => (false, true),
298 _ => panic!("Error in regular expression for flags"),
299 }
300 } else {
301 (true, false)
302 };
303
304 let browse_name = captures.name("name").unwrap().as_str();
305
306 let reference_type_id = if let Some(namespace) = captures.name("nsidx") {
308 let namespace = namespace.as_str();
309 if namespace == "0" || namespace.is_empty() {
310 node_resolver(0, browse_name)
311 } else if let Ok(namespace) = namespace.parse::<u16>() {
312 node_resolver(namespace, browse_name)
313 } else {
314 error!("Namespace {} is out of range", namespace);
315 return Err(RelativePathError::NamespaceOutOfRange);
316 }
317 } else {
318 node_resolver(0, browse_name)
319 };
320 if reference_type_id.is_none() {
321 error!(
322 "Supplied node resolver was unable to resolve a reference type from {}",
323 path
324 );
325 return Err(RelativePathError::UnresolvedReferenceType);
326 }
327 (reference_type_id.unwrap(), include_subtypes, is_inverse)
328 }
329 };
330 Ok(RelativePathElement {
331 reference_type_id,
332 is_inverse,
333 include_subtypes,
334 target_name,
335 })
336 } else {
337 error!("Path {} does not match a relative path", path);
338 Err(RelativePathError::NoMatch)
339 }
340 }
341
342 pub(crate) fn relative_path_reference_type<CB>(&self, browse_name_resolver: &CB) -> String
346 where
347 CB: Fn(&NodeId) -> Option<String>,
348 {
349 let browse_name = browse_name_resolver(&self.reference_type_id).unwrap();
350 let mut result = String::with_capacity(1024);
351 if self.include_subtypes && !self.is_inverse {
353 if self.reference_type_id == ReferenceTypeId::HierarchicalReferences {
354 result.push('/');
355 } else if self.reference_type_id == ReferenceTypeId::Aggregates {
356 result.push('.');
357 }
358 };
359 if result.is_empty() {
361 result.push('<');
362 if !self.include_subtypes {
363 result.push('#');
364 }
365 if self.is_inverse {
366 result.push('!');
367 }
368
369 let browse_name = escape_browse_name(browse_name.as_ref());
370 if self.reference_type_id.namespace != 0 {
371 result.push_str(&format!(
372 "{}:{}",
373 self.reference_type_id.namespace, browse_name
374 ));
375 } else {
376 result.push_str(&browse_name);
377 }
378 result.push('>');
379 }
380
381 result
382 }
383}
384
385#[allow(missing_docs)]
387#[derive(Error, Debug)]
388pub enum RelativePathError {
389 #[error("Namespace is out of range of a u16.")]
390 NamespaceOutOfRange,
391 #[error("Supplied node resolver was unable to resolve a reference type.")]
392 UnresolvedReferenceType,
393 #[error("Path does not match a relative path.")]
394 NoMatch,
395 #[error("Path segment is unusually long and has been rejected.")]
396 PathSegmentTooLong,
397 #[error("Number of elements in relative path is too large.")]
398 TooManyElements,
399}
400
401impl<'a> From<&'a RelativePath> for String {
402 fn from(path: &'a RelativePath) -> String {
403 if let Some(ref elements) = path.elements {
404 let mut result = String::with_capacity(1024);
405 for e in elements.iter() {
406 result.push_str(String::from(e).as_ref());
407 }
408 result
409 } else {
410 String::new()
411 }
412 }
413}
414
415const BROWSE_NAME_RESERVED_CHARS: &str = "&/.<>:#!";
417
418fn escape_browse_name(name: &str) -> String {
420 let mut result = String::from(name);
421 BROWSE_NAME_RESERVED_CHARS.chars().for_each(|c| {
422 result = result.replace(c, &format!("&{c}"));
423 });
424 result
425}
426
427fn unescape_browse_name(name: &str) -> String {
429 let mut result = String::from(name);
430 BROWSE_NAME_RESERVED_CHARS.chars().for_each(|c| {
431 result = result.replace(&format!("&{c}"), &c.to_string());
432 });
433 result
434}
435
436fn target_name(target_name: &str) -> Result<QualifiedName, RelativePathError> {
445 static RE: LazyLock<Regex> =
446 LazyLock::new(|| Regex::new(r"((?P<nsidx>[0-9+]):)?(?P<name>.*)").unwrap());
447 if let Some(captures) = RE.captures(target_name) {
448 let namespace = if let Some(namespace) = captures.name("nsidx") {
449 if let Ok(namespace) = namespace.as_str().parse::<u16>() {
450 namespace
451 } else {
452 error!(
453 "Namespace {} for target name is out of range",
454 namespace.as_str()
455 );
456 return Err(RelativePathError::NamespaceOutOfRange);
457 }
458 } else {
459 0
460 };
461 let name = if let Some(name) = captures.name("name") {
462 let name = name.as_str();
463 if name.is_empty() {
464 UAString::null()
465 } else {
466 UAString::from(unescape_browse_name(name))
467 }
468 } else {
469 UAString::null()
470 };
471 Ok(QualifiedName::new(namespace, name))
472 } else {
473 Ok(QualifiedName::null())
474 }
475}
476
477#[test]
479fn test_escape_browse_name() {
480 [
481 ("", ""),
482 ("Hello World", "Hello World"),
483 ("Hello &World", "Hello &&World"),
484 ("Hello &&World", "Hello &&&&World"),
485 ("Block.Output", "Block&.Output"),
486 ("/Name_1", "&/Name_1"),
487 (".Name_2", "&.Name_2"),
488 (":Name_3", "&:Name_3"),
489 ("&Name_4", "&&Name_4"),
490 ]
491 .iter()
492 .for_each(|n| {
493 let original = n.0.to_string();
494 let escaped = n.1.to_string();
495 assert_eq!(escaped, escape_browse_name(&original));
496 assert_eq!(unescape_browse_name(&escaped), original);
497 });
498}
499
500#[test]
503fn test_relative_path_element() {
504 use crate::qualified_name::QualifiedName;
505
506 [
507 (
508 RelativePathElement {
509 reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
510 is_inverse: false,
511 include_subtypes: true,
512 target_name: QualifiedName::new(0, "foo1"),
513 },
514 "/0:foo1",
515 ),
516 (
517 RelativePathElement {
518 reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
519 is_inverse: false,
520 include_subtypes: true,
521 target_name: QualifiedName::new(0, ".foo2"),
522 },
523 "/0:&.foo2",
524 ),
525 (
526 RelativePathElement {
527 reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
528 is_inverse: true,
529 include_subtypes: true,
530 target_name: QualifiedName::new(2, "foo3"),
531 },
532 "<!HierarchicalReferences>2:foo3",
533 ),
534 (
535 RelativePathElement {
536 reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
537 is_inverse: true,
538 include_subtypes: false,
539 target_name: QualifiedName::new(0, "foo4"),
540 },
541 "<#!HierarchicalReferences>0:foo4",
542 ),
543 (
544 RelativePathElement {
545 reference_type_id: ReferenceTypeId::Aggregates.into(),
546 is_inverse: false,
547 include_subtypes: true,
548 target_name: QualifiedName::new(0, "foo5"),
549 },
550 ".0:foo5",
551 ),
552 (
553 RelativePathElement {
554 reference_type_id: ReferenceTypeId::HasHistoricalConfiguration.into(),
555 is_inverse: false,
556 include_subtypes: true,
557 target_name: QualifiedName::new(0, "foo6"),
558 },
559 "<HasHistoricalConfiguration>0:foo6",
560 ),
561 ]
562 .iter()
563 .for_each(|n| {
564 let element = &n.0;
565 let expected = n.1.to_string();
566
567 let actual = String::from(element);
569 assert_eq!(expected, actual);
570
571 let actual =
573 RelativePathElement::from_str(&actual, &RelativePathElement::default_node_resolver)
574 .unwrap();
575 assert_eq!(*element, actual);
576 });
577}
578
579#[test]
582fn test_relative_path() {
583 use crate::qualified_name::QualifiedName;
584
585 let tests = vec![
587 (
588 vec![RelativePathElement {
589 reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
590 is_inverse: false,
591 include_subtypes: true,
592 target_name: QualifiedName::new(2, "Block.Output"),
593 }],
594 "/2:Block&.Output",
595 ),
596 (
597 vec![
598 RelativePathElement {
599 reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
600 is_inverse: false,
601 include_subtypes: true,
602 target_name: QualifiedName::new(3, "Truck"),
603 },
604 RelativePathElement {
605 reference_type_id: ReferenceTypeId::Aggregates.into(),
606 is_inverse: false,
607 include_subtypes: true,
608 target_name: QualifiedName::new(0, "NodeVersion"),
609 },
610 ],
611 "/3:Truck.0:NodeVersion",
612 ),
613 (
614 vec![
615 RelativePathElement {
616 reference_type_id: NodeId::new(1, "ConnectedTo"),
617 is_inverse: false,
618 include_subtypes: true,
619 target_name: QualifiedName::new(1, "Boiler"),
620 },
621 RelativePathElement {
622 reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
623 is_inverse: false,
624 include_subtypes: true,
625 target_name: QualifiedName::new(1, "HeatSensor"),
626 },
627 ],
628 "<1:ConnectedTo>1:Boiler/1:HeatSensor",
629 ),
630 (
631 vec![
632 RelativePathElement {
633 reference_type_id: NodeId::new(1, "ConnectedTo"),
634 is_inverse: false,
635 include_subtypes: true,
636 target_name: QualifiedName::new(1, "Boiler"),
637 },
638 RelativePathElement {
639 reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
640 is_inverse: false,
641 include_subtypes: true,
642 target_name: QualifiedName::null(),
643 },
644 ],
645 "<1:ConnectedTo>1:Boiler/",
646 ),
647 (
648 vec![RelativePathElement {
649 reference_type_id: ReferenceTypeId::HasChild.into(),
650 is_inverse: false,
651 include_subtypes: true,
652 target_name: QualifiedName::new(2, "Wheel"),
653 }],
654 "<HasChild>2:Wheel",
655 ),
656 (
657 vec![RelativePathElement {
658 reference_type_id: ReferenceTypeId::HasChild.into(),
659 is_inverse: true,
660 include_subtypes: true,
661 target_name: QualifiedName::new(0, "Truck"),
662 }],
663 "<!HasChild>0:Truck",
664 ),
665 (
666 vec![RelativePathElement {
667 reference_type_id: ReferenceTypeId::HasChild.into(),
668 is_inverse: false,
669 include_subtypes: true,
670 target_name: QualifiedName::null(),
671 }],
672 "<HasChild>",
673 ),
674 ];
675
676 tests.into_iter().for_each(|n| {
677 let relative_path = RelativePath {
678 elements: Some(n.0),
679 };
680 let expected = n.1.to_string();
681
682 let actual = String::from(&relative_path);
684 assert_eq!(expected, actual);
685
686 let actual =
688 RelativePath::from_str(&actual, &RelativePathElement::default_node_resolver).unwrap();
689 assert_eq!(relative_path, actual);
690 });
691}