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