Skip to main content

opcua/types/
relative_path.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2022 Adam Lock
4
5//! Contains functions used for making relative paths from / to strings, as per OPC UA Part 4, Appendix A
6//!
7//! Functions are implemented on the `RelativePath` and `RelativePathElement` structs where
8//! there are most useful.
9//!
10use std::{error::Error, fmt};
11
12use regex::Regex;
13
14use crate::types::{
15    node_id::{Identifier, NodeId},
16    node_ids::*,
17    qualified_name::QualifiedName,
18    service_types::{RelativePath, RelativePathElement},
19    string::UAString,
20};
21
22#[derive(Debug)]
23struct RelativePathError;
24
25impl fmt::Display for RelativePathError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "RelativePathError")
28    }
29}
30
31impl Error for RelativePathError {}
32
33impl RelativePath {
34    /// The maximum size in chars of any path element.
35    const MAX_TOKEN_LEN: usize = 256;
36    /// The maximum number of elements in total.
37    const MAX_ELEMENTS: usize = 32;
38
39    /// Converts a string into a relative path. Caller must supply a `node_resolver` which will
40    /// be used to look up nodes from their browse name. The function will reject strings
41    /// that look unusually long or contain too many elements.
42    pub fn from_str<CB>(path: &str, node_resolver: &CB) -> Result<RelativePath, ()>
43    where
44        CB: Fn(u16, &str) -> Option<NodeId>,
45    {
46        let mut elements: Vec<RelativePathElement> = Vec::new();
47
48        // This loop will break the string up into path segments. For each segment it will
49        // then parse it into a relative path element. When the string is successfully parsed,
50        // the elements will be returned.
51        let mut escaped_char = false;
52        let mut token = String::with_capacity(path.len());
53        for c in path.chars() {
54            if escaped_char {
55                token.push(c);
56                escaped_char = false;
57            } else {
58                // Parse the
59                match c {
60                    '&' => {
61                        // The next character is escaped and part of the token
62                        escaped_char = true;
63                    }
64                    '/' | '.' | '<' => {
65                        // We have reached the start of a token and need to process the previous one
66                        if !token.is_empty() {
67                            if elements.len() == Self::MAX_ELEMENTS {
68                                break;
69                            }
70                            elements.push(RelativePathElement::from_str(&token, node_resolver)?);
71                            token.clear();
72                        }
73                    }
74                    _ => {}
75                }
76                token.push(c);
77            }
78            if token.len() > Self::MAX_TOKEN_LEN {
79                error!("Path segment seems unusually long and has been rejected");
80                return Err(());
81            }
82        }
83
84        if !token.is_empty() {
85            if elements.len() == Self::MAX_ELEMENTS {
86                error!("Number of elements in relative path is too long, rejecting it");
87                return Err(());
88            }
89            elements.push(RelativePathElement::from_str(&token, node_resolver)?);
90        }
91
92        Ok(RelativePath {
93            elements: Some(elements),
94        })
95    }
96}
97
98impl<'a> From<&'a RelativePathElement> for String {
99    fn from(element: &'a RelativePathElement) -> String {
100        let mut result = element
101            .relative_path_reference_type(&RelativePathElement::default_browse_name_resolver);
102        if !element.target_name.name.is_null() {
103            let always_use_namespace = true;
104            let target_browse_name = escape_browse_name(element.target_name.name.as_ref());
105            if always_use_namespace || element.target_name.namespace_index > 0 {
106                result.push_str(&format!(
107                    "{}:{}",
108                    element.target_name.namespace_index, target_browse_name
109                ));
110            } else {
111                result.push_str(&target_browse_name);
112            }
113        }
114        result
115    }
116}
117
118impl RelativePathElement {
119    /// This is the default node resolver that attempts to resolve a browse name onto a
120    /// reference type id. The default implementation resides in the types module so it
121    /// doesn't have access to the address space.
122    ///
123    /// Therefore it makes a best guess by testing the browse name against the standard reference
124    /// types and if fails to match it will produce a node id from the namespace and browse name.
125    pub fn default_node_resolver(namespace: u16, browse_name: &str) -> Option<NodeId> {
126        let node_id = if namespace == 0 {
127            match browse_name {
128                "References" => ReferenceTypeId::References.into(),
129                "NonHierarchicalReferences" => ReferenceTypeId::NonHierarchicalReferences.into(),
130                "HierarchicalReferences" => ReferenceTypeId::HierarchicalReferences.into(),
131                "HasChild" => ReferenceTypeId::HasChild.into(),
132                "Organizes" => ReferenceTypeId::Organizes.into(),
133                "HasEventSource" => ReferenceTypeId::HasEventSource.into(),
134                "HasModellingRule" => ReferenceTypeId::HasModellingRule.into(),
135                "HasEncoding" => ReferenceTypeId::HasEncoding.into(),
136                "HasDescription" => ReferenceTypeId::HasDescription.into(),
137                "HasTypeDefinition" => ReferenceTypeId::HasTypeDefinition.into(),
138                "GeneratesEvent" => ReferenceTypeId::GeneratesEvent.into(),
139                "Aggregates" => ReferenceTypeId::Aggregates.into(),
140                "HasSubtype" => ReferenceTypeId::HasSubtype.into(),
141                "HasProperty" => ReferenceTypeId::HasProperty.into(),
142                "HasComponent" => ReferenceTypeId::HasComponent.into(),
143                "HasNotifier" => ReferenceTypeId::HasNotifier.into(),
144                "HasOrderedComponent" => ReferenceTypeId::HasOrderedComponent.into(),
145                "FromState" => ReferenceTypeId::FromState.into(),
146                "ToState" => ReferenceTypeId::ToState.into(),
147                "HasCause" => ReferenceTypeId::HasCause.into(),
148                "HasEffect" => ReferenceTypeId::HasEffect.into(),
149                "HasHistoricalConfiguration" => ReferenceTypeId::HasHistoricalConfiguration.into(),
150                "HasSubStateMachine" => ReferenceTypeId::HasSubStateMachine.into(),
151                "AlwaysGeneratesEvent" => ReferenceTypeId::AlwaysGeneratesEvent.into(),
152                "HasTrueSubState" => ReferenceTypeId::HasTrueSubState.into(),
153                "HasFalseSubState" => ReferenceTypeId::HasFalseSubState.into(),
154                "HasCondition" => ReferenceTypeId::HasCondition.into(),
155                _ => NodeId::new(0, UAString::from(browse_name)),
156            }
157        } else {
158            NodeId::new(namespace, UAString::from(browse_name))
159        };
160        Some(node_id)
161    }
162
163    fn id_from_reference_type(id: u32) -> Option<String> {
164        // This syntax is horrible - it casts the u32 into an enum if it can
165        Some(
166            match id {
167                id if id == ReferenceTypeId::References as u32 => "References",
168                id if id == ReferenceTypeId::NonHierarchicalReferences as u32 => {
169                    "NonHierarchicalReferences"
170                }
171                id if id == ReferenceTypeId::HierarchicalReferences as u32 => {
172                    "HierarchicalReferences"
173                }
174                id if id == ReferenceTypeId::HasChild as u32 => "HasChild",
175                id if id == ReferenceTypeId::Organizes as u32 => "Organizes",
176                id if id == ReferenceTypeId::HasEventSource as u32 => "HasEventSource",
177                id if id == ReferenceTypeId::HasModellingRule as u32 => "HasModellingRule",
178                id if id == ReferenceTypeId::HasEncoding as u32 => "HasEncoding",
179                id if id == ReferenceTypeId::HasDescription as u32 => "HasDescription",
180                id if id == ReferenceTypeId::HasTypeDefinition as u32 => "HasTypeDefinition",
181                id if id == ReferenceTypeId::GeneratesEvent as u32 => "GeneratesEvent",
182                id if id == ReferenceTypeId::Aggregates as u32 => "Aggregates",
183                id if id == ReferenceTypeId::HasSubtype as u32 => "HasSubtype",
184                id if id == ReferenceTypeId::HasProperty as u32 => "HasProperty",
185                id if id == ReferenceTypeId::HasComponent as u32 => "HasComponent",
186                id if id == ReferenceTypeId::HasNotifier as u32 => "HasNotifier",
187                id if id == ReferenceTypeId::HasOrderedComponent as u32 => "HasOrderedComponent",
188                id if id == ReferenceTypeId::FromState as u32 => "FromState",
189                id if id == ReferenceTypeId::ToState as u32 => "ToState",
190                id if id == ReferenceTypeId::HasCause as u32 => "HasCause",
191                id if id == ReferenceTypeId::HasEffect as u32 => "HasEffect",
192                id if id == ReferenceTypeId::HasHistoricalConfiguration as u32 => {
193                    "HasHistoricalConfiguration"
194                }
195                id if id == ReferenceTypeId::HasSubStateMachine as u32 => "HasSubStateMachine",
196                id if id == ReferenceTypeId::AlwaysGeneratesEvent as u32 => "AlwaysGeneratesEvent",
197                id if id == ReferenceTypeId::HasTrueSubState as u32 => "HasTrueSubState",
198                id if id == ReferenceTypeId::HasFalseSubState as u32 => "HasFalseSubState",
199                id if id == ReferenceTypeId::HasCondition as u32 => "HasCondition",
200                _ => return None,
201            }
202            .to_string(),
203        )
204    }
205
206    pub fn default_browse_name_resolver(node_id: &NodeId) -> Option<String> {
207        match &node_id.identifier {
208            Identifier::String(browse_name) => Some(browse_name.as_ref().to_string()),
209            Identifier::Numeric(id) => {
210                if node_id.namespace == 0 {
211                    Self::id_from_reference_type(*id)
212                } else {
213                    None
214                }
215            }
216            _ => None,
217        }
218    }
219
220    /// Parse a relative path element according to the OPC UA Part 4 Appendix A BNF
221    ///
222    /// `<relative-path> ::= <reference-type> <browse-name> [relative-path]`
223    /// `<reference-type> ::= '/' | '.' | '<' ['#'] ['!'] <browse-name> '>'`
224    /// `<browse-name> ::= [<namespace-index> ':'] <name>`
225    /// `<namespace-index> ::= <digit> [<digit>]`
226    /// `<digit> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'`
227    /// `<name> ::= (<name-char> | '&' <reserved-char>) [<name>]`
228    /// `<reserved-char> ::= '/' | '.' | '<' | '>' | ':' | '#' | '!' | '&'`
229    /// `<name-char> ::= All valid characters for a String (see Part 3) excluding reserved-chars.`
230    ///
231    /// # Examples
232    ///
233    /// * `/foo`
234    /// * `/0:foo`
235    /// * `.bar`
236    /// * `<0:HasEncoding>bar`
237    /// * `<!NonHierarchicalReferences>foo`
238    /// * `<#!2:MyReftype>2:blah`
239    ///
240    pub fn from_str<CB>(path: &str, node_resolver: &CB) -> Result<RelativePathElement, ()>
241    where
242        CB: Fn(u16, &str) -> Option<NodeId>,
243    {
244        lazy_static! {
245            static ref RE: Regex = Regex::new(r"(?P<reftype>/|\.|(<(?P<flags>#|!|#!)?((?P<nsidx>[0-9]+):)?(?P<name>[^#!].*)>))(?P<target>.*)").unwrap();
246        }
247
248        // NOTE: This could be more safely done with a parser library, e.g. nom.
249
250        if let Some(captures) = RE.captures(path) {
251            let target_name = target_name(captures.name("target").unwrap().as_str())?;
252
253            let reference_type = captures.name("reftype").unwrap();
254            let (reference_type_id, include_subtypes, is_inverse) = match reference_type.as_str() {
255                "/" => (ReferenceTypeId::HierarchicalReferences.into(), true, false),
256                "." => (ReferenceTypeId::Aggregates.into(), true, false),
257                _ => {
258                    let (include_subtypes, is_inverse) = if let Some(flags) = captures.name("flags")
259                    {
260                        match flags.as_str() {
261                            "#" => (false, false),
262                            "!" => (true, true),
263                            "#!" => (false, true),
264                            _ => panic!("Error in regular expression for flags"),
265                        }
266                    } else {
267                        (true, false)
268                    };
269
270                    let browse_name = captures.name("name").unwrap().as_str();
271
272                    // Process the token as a reference type
273                    let reference_type_id = if let Some(namespace) = captures.name("nsidx") {
274                        let namespace = namespace.as_str();
275                        if namespace == "0" || namespace.is_empty() {
276                            node_resolver(0, browse_name)
277                        } else if let Ok(namespace) = namespace.parse::<u16>() {
278                            node_resolver(namespace, browse_name)
279                        } else {
280                            error!("Namespace {} is out of range", namespace);
281                            return Err(());
282                        }
283                    } else {
284                        node_resolver(0, browse_name)
285                    };
286                    if reference_type_id.is_none() {
287                        error!(
288                            "Supplied node resolver was unable to resolve a reference type from {}",
289                            path
290                        );
291                        return Err(());
292                    }
293                    (reference_type_id.unwrap(), include_subtypes, is_inverse)
294                }
295            };
296            Ok(RelativePathElement {
297                reference_type_id,
298                is_inverse,
299                include_subtypes,
300                target_name,
301            })
302        } else {
303            error!("Path {} does not match a relative path", path);
304            Err(())
305        }
306    }
307
308    /// Constructs a string representation of the reference type in the relative path.
309    /// This code assumes that the reference type's node id has a string identifier and that
310    /// the string identifier is the same as the browse name.
311    pub(crate) fn relative_path_reference_type<CB>(&self, browse_name_resolver: &CB) -> String
312    where
313        CB: Fn(&NodeId) -> Option<String>,
314    {
315        let browse_name = browse_name_resolver(&self.reference_type_id).unwrap();
316        let mut result = String::with_capacity(1024);
317        // Common references will come out as '/' or '.'
318        if self.include_subtypes && !self.is_inverse {
319            if self.reference_type_id == ReferenceTypeId::HierarchicalReferences.into() {
320                result.push('/');
321            } else if self.reference_type_id == ReferenceTypeId::Aggregates.into() {
322                result.push('.');
323            }
324        };
325        // Other kinds of reference are built as a string
326        if result.is_empty() {
327            result.push('<');
328            if !self.include_subtypes {
329                result.push('#');
330            }
331            if self.is_inverse {
332                result.push('!');
333            }
334
335            let browse_name = escape_browse_name(browse_name.as_ref());
336            if self.reference_type_id.namespace != 0 {
337                result.push_str(&format!(
338                    "{}:{}",
339                    self.reference_type_id.namespace, browse_name
340                ));
341            } else {
342                result.push_str(&browse_name);
343            }
344            result.push('>');
345        }
346
347        result
348    }
349}
350
351impl<'a> From<&'a RelativePath> for String {
352    fn from(path: &'a RelativePath) -> String {
353        if let Some(ref elements) = path.elements {
354            let mut result = String::with_capacity(1024);
355            for e in elements.iter() {
356                result.push_str(String::from(e).as_ref());
357            }
358            result
359        } else {
360            String::new()
361        }
362    }
363}
364
365/// Reserved characters in the browse name which must be escaped with a &
366const BROWSE_NAME_RESERVED_CHARS: &str = "&/.<>:#!";
367
368/// Escapes reserved characters in the browse name
369fn escape_browse_name(name: &str) -> String {
370    let mut result = String::from(name);
371    BROWSE_NAME_RESERVED_CHARS.chars().for_each(|c| {
372        result = result.replace(c, &format!("&{}", c));
373    });
374    result
375}
376
377/// Unescapes reserved characters in the browse name
378fn unescape_browse_name(name: &str) -> String {
379    let mut result = String::from(name);
380    BROWSE_NAME_RESERVED_CHARS.chars().for_each(|c| {
381        result = result.replace(&format!("&{}", c), &c.to_string());
382    });
383    result
384}
385
386/// Parse a target name into a qualified name. The name is either `nsidx:name` or just
387/// `name`, where `nsidx` is a numeric index and `name` may contain escaped reserved chars.
388///
389/// # Examples
390///
391/// * 0:foo
392/// * bar
393///
394fn target_name(target_name: &str) -> Result<QualifiedName, ()> {
395    lazy_static! {
396        static ref RE: Regex = Regex::new(r"((?P<nsidx>[0-9+]):)?(?P<name>.*)").unwrap();
397    }
398    if let Some(captures) = RE.captures(target_name) {
399        let namespace = if let Some(namespace) = captures.name("nsidx") {
400            if let Ok(namespace) = namespace.as_str().parse::<u16>() {
401                namespace
402            } else {
403                error!(
404                    "Namespace {} for target name is out of range",
405                    namespace.as_str()
406                );
407                return Err(());
408            }
409        } else {
410            0
411        };
412        let name = if let Some(name) = captures.name("name") {
413            let name = name.as_str();
414            if name.is_empty() {
415                UAString::null()
416            } else {
417                UAString::from(unescape_browse_name(name))
418            }
419        } else {
420            UAString::null()
421        };
422        Ok(QualifiedName::new(namespace, name))
423    } else {
424        Ok(QualifiedName::null())
425    }
426}
427
428/// Test that escaping of browse names works as expected in each direction
429#[test]
430fn test_escape_browse_name() {
431    [
432        ("", ""),
433        ("Hello World", "Hello World"),
434        ("Hello &World", "Hello &&World"),
435        ("Hello &&World", "Hello &&&&World"),
436        ("Block.Output", "Block&.Output"),
437        ("/Name_1", "&/Name_1"),
438        (".Name_2", "&.Name_2"),
439        (":Name_3", "&:Name_3"),
440        ("&Name_4", "&&Name_4"),
441    ]
442    .iter()
443    .for_each(|n| {
444        let original = n.0.to_string();
445        let escaped = n.1.to_string();
446        assert_eq!(escaped, escape_browse_name(&original));
447        assert_eq!(unescape_browse_name(&escaped), original);
448    });
449}
450
451/// Test that given a relative path element that it can be converted to/from a string
452/// and a RelativePathElement type
453#[test]
454fn test_relative_path_element() {
455    use crate::types::qualified_name::QualifiedName;
456
457    [
458        (
459            RelativePathElement {
460                reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
461                is_inverse: false,
462                include_subtypes: true,
463                target_name: QualifiedName::new(0, "foo1"),
464            },
465            "/0:foo1",
466        ),
467        (
468            RelativePathElement {
469                reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
470                is_inverse: false,
471                include_subtypes: true,
472                target_name: QualifiedName::new(0, ".foo2"),
473            },
474            "/0:&.foo2",
475        ),
476        (
477            RelativePathElement {
478                reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
479                is_inverse: true,
480                include_subtypes: true,
481                target_name: QualifiedName::new(2, "foo3"),
482            },
483            "<!HierarchicalReferences>2:foo3",
484        ),
485        (
486            RelativePathElement {
487                reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
488                is_inverse: true,
489                include_subtypes: false,
490                target_name: QualifiedName::new(0, "foo4"),
491            },
492            "<#!HierarchicalReferences>0:foo4",
493        ),
494        (
495            RelativePathElement {
496                reference_type_id: ReferenceTypeId::Aggregates.into(),
497                is_inverse: false,
498                include_subtypes: true,
499                target_name: QualifiedName::new(0, "foo5"),
500            },
501            ".0:foo5",
502        ),
503        (
504            RelativePathElement {
505                reference_type_id: ReferenceTypeId::HasHistoricalConfiguration.into(),
506                is_inverse: false,
507                include_subtypes: true,
508                target_name: QualifiedName::new(0, "foo6"),
509            },
510            "<HasHistoricalConfiguration>0:foo6",
511        ),
512    ]
513    .iter()
514    .for_each(|n| {
515        let element = &n.0;
516        let expected = n.1.to_string();
517
518        // Compare string to expected
519        let actual = String::from(element);
520        assert_eq!(expected, actual);
521
522        // Turn string back to element, compare to original element
523        let actual =
524            RelativePathElement::from_str(&actual, &RelativePathElement::default_node_resolver)
525                .unwrap();
526        assert_eq!(*element, actual);
527    });
528}
529
530/// Test that the given entire relative path, that it can be converted to/from a string
531/// and a RelativePath type.
532#[test]
533fn test_relative_path() {
534    use crate::types::qualified_name::QualifiedName;
535
536    // Samples are from OPC UA Part 4 Appendix A
537    let tests = vec![
538        (
539            vec![RelativePathElement {
540                reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
541                is_inverse: false,
542                include_subtypes: true,
543                target_name: QualifiedName::new(2, "Block.Output"),
544            }],
545            "/2:Block&.Output",
546        ),
547        (
548            vec![
549                RelativePathElement {
550                    reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
551                    is_inverse: false,
552                    include_subtypes: true,
553                    target_name: QualifiedName::new(3, "Truck"),
554                },
555                RelativePathElement {
556                    reference_type_id: ReferenceTypeId::Aggregates.into(),
557                    is_inverse: false,
558                    include_subtypes: true,
559                    target_name: QualifiedName::new(0, "NodeVersion"),
560                },
561            ],
562            "/3:Truck.0:NodeVersion",
563        ),
564        (
565            vec![
566                RelativePathElement {
567                    reference_type_id: NodeId::new(1, "ConnectedTo"),
568                    is_inverse: false,
569                    include_subtypes: true,
570                    target_name: QualifiedName::new(1, "Boiler"),
571                },
572                RelativePathElement {
573                    reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
574                    is_inverse: false,
575                    include_subtypes: true,
576                    target_name: QualifiedName::new(1, "HeatSensor"),
577                },
578            ],
579            "<1:ConnectedTo>1:Boiler/1:HeatSensor",
580        ),
581        (
582            vec![
583                RelativePathElement {
584                    reference_type_id: NodeId::new(1, "ConnectedTo"),
585                    is_inverse: false,
586                    include_subtypes: true,
587                    target_name: QualifiedName::new(1, "Boiler"),
588                },
589                RelativePathElement {
590                    reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
591                    is_inverse: false,
592                    include_subtypes: true,
593                    target_name: QualifiedName::null(),
594                },
595            ],
596            "<1:ConnectedTo>1:Boiler/",
597        ),
598        (
599            vec![RelativePathElement {
600                reference_type_id: ReferenceTypeId::HasChild.into(),
601                is_inverse: false,
602                include_subtypes: true,
603                target_name: QualifiedName::new(2, "Wheel"),
604            }],
605            "<HasChild>2:Wheel",
606        ),
607        (
608            vec![RelativePathElement {
609                reference_type_id: ReferenceTypeId::HasChild.into(),
610                is_inverse: true,
611                include_subtypes: true,
612                target_name: QualifiedName::new(0, "Truck"),
613            }],
614            "<!HasChild>0:Truck",
615        ),
616        (
617            vec![RelativePathElement {
618                reference_type_id: ReferenceTypeId::HasChild.into(),
619                is_inverse: false,
620                include_subtypes: true,
621                target_name: QualifiedName::null(),
622            }],
623            "<HasChild>",
624        ),
625    ];
626
627    tests.into_iter().for_each(|n| {
628        let relative_path = RelativePath {
629            elements: Some(n.0),
630        };
631        let expected = n.1.to_string();
632
633        // Convert path to string, compare to expected
634        let actual = String::from(&relative_path);
635        assert_eq!(expected, actual);
636
637        // Turn string back to element, compare to original path
638        let actual =
639            RelativePath::from_str(&actual, &RelativePathElement::default_node_resolver).unwrap();
640        assert_eq!(relative_path, actual);
641    });
642}