opcua_types/
qualified_name.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2024 Adam Lock
4
5//! Contains the definition of `QualifiedName`.
6use std::{
7    fmt::Display,
8    io::{Read, Write},
9    sync::LazyLock,
10};
11
12use percent_encoding_rfc3986::percent_decode_str;
13use regex::Regex;
14
15use crate::{
16    encoding::{BinaryDecodable, BinaryEncodable, EncodingResult},
17    string::*,
18    NamespaceMap, UaNullable,
19};
20
21#[allow(unused)]
22mod opcua {
23    pub(super) use crate as types;
24}
25
26/// An identifier for a error or condition that is associated with a value or an operation.
27///
28/// A name qualified by a namespace.
29///
30/// For JSON, the namespace_index is saved as "Uri" and MUST be a numeric value or it will not parse. This is
31/// is in accordance with OPC UA spec that says to save the index as a numeric according to rules cut and
32/// pasted from spec below:
33///
34/// Name   The Name component of the QualifiedName.
35///
36/// Uri    The _NamespaceIndexcomponent_ of the QualifiedNameencoded as a JSON number. The Urifield
37///        is omitted if the NamespaceIndex equals 0. For the non-reversible form, the
38///        NamespaceUriassociated with the NamespaceIndexportion of the QualifiedNameis encoded as
39///        JSON string unless the NamespaceIndexis 1 or if NamespaceUriis unknown. In these cases,
40///        the NamespaceIndexis encoded as a JSON number.
41#[derive(PartialEq, Debug, Clone, Eq, Hash)]
42pub struct QualifiedName {
43    /// The namespace index
44    pub namespace_index: u16,
45    /// The name.
46    pub name: UAString,
47}
48
49impl UaNullable for QualifiedName {
50    fn is_ua_null(&self) -> bool {
51        self.is_null()
52    }
53}
54
55#[cfg(feature = "xml")]
56mod xml {
57    use crate::{xml::*, UAString};
58
59    use super::QualifiedName;
60
61    impl XmlType for QualifiedName {
62        const TAG: &'static str = "QualifiedName";
63    }
64
65    impl XmlEncodable for QualifiedName {
66        fn encode(
67            &self,
68            writer: &mut XmlStreamWriter<&mut dyn std::io::Write>,
69            context: &Context<'_>,
70        ) -> EncodingResult<()> {
71            let namespace_index = context.resolve_namespace_index_inverse(self.namespace_index)?;
72            writer.encode_child("NamespaceIndex", &namespace_index, context)?;
73            writer.encode_child("Name", &self.name, context)?;
74            Ok(())
75        }
76    }
77
78    impl XmlDecodable for QualifiedName {
79        fn decode(
80            read: &mut XmlStreamReader<&mut dyn std::io::Read>,
81            context: &Context<'_>,
82        ) -> Result<Self, Error> {
83            let mut namespace_index = None;
84            let mut name: Option<UAString> = None;
85
86            read.iter_children(
87                |key, stream, ctx| {
88                    match key.as_str() {
89                        "NamespaceIndex" => {
90                            namespace_index = Some(XmlDecodable::decode(stream, ctx)?)
91                        }
92                        "Name" => name = Some(XmlDecodable::decode(stream, ctx)?),
93                        _ => {
94                            stream.skip_value()?;
95                        }
96                    }
97                    Ok(())
98                },
99                context,
100            )?;
101
102            let Some(name) = name else {
103                return Ok(QualifiedName::null());
104            };
105
106            if let Some(namespace_index) = namespace_index {
107                Ok(QualifiedName {
108                    namespace_index: context.resolve_namespace_index(namespace_index)?,
109                    name,
110                })
111            } else {
112                Ok(QualifiedName::new(0, name))
113            }
114        }
115    }
116}
117
118#[cfg(feature = "json")]
119mod json {
120    use super::QualifiedName;
121
122    use crate::json::*;
123
124    // JSON encoding for QualifiedName is special, see 5.3.1.14.
125    impl JsonEncodable for QualifiedName {
126        fn encode(
127            &self,
128            stream: &mut JsonStreamWriter<&mut dyn std::io::Write>,
129            _ctx: &crate::Context<'_>,
130        ) -> crate::EncodingResult<()> {
131            if self.is_null() {
132                stream.null_value()?;
133                return Ok(());
134            }
135            stream.string_value(&self.to_string())?;
136            Ok(())
137        }
138    }
139
140    impl JsonDecodable for QualifiedName {
141        fn decode(
142            stream: &mut JsonStreamReader<&mut dyn std::io::Read>,
143            ctx: &Context<'_>,
144        ) -> crate::EncodingResult<Self> {
145            if matches!(stream.peek()?, ValueType::Null) {
146                return Ok(QualifiedName::null());
147            }
148
149            let raw = stream.next_str()?;
150            Ok(QualifiedName::parse(raw, ctx.namespaces()))
151        }
152    }
153}
154
155impl Default for QualifiedName {
156    fn default() -> Self {
157        Self::null()
158    }
159}
160
161impl<'a> From<&'a str> for QualifiedName {
162    fn from(value: &'a str) -> Self {
163        Self {
164            namespace_index: 0,
165            name: UAString::from(value),
166        }
167    }
168}
169
170impl From<&String> for QualifiedName {
171    fn from(value: &String) -> Self {
172        Self {
173            namespace_index: 0,
174            name: UAString::from(value),
175        }
176    }
177}
178
179impl From<String> for QualifiedName {
180    fn from(value: String) -> Self {
181        Self {
182            namespace_index: 0,
183            name: UAString::from(value),
184        }
185    }
186}
187
188impl BinaryEncodable for QualifiedName {
189    fn byte_len(&self, ctx: &opcua::types::Context<'_>) -> usize {
190        let mut size: usize = 0;
191        size += self.namespace_index.byte_len(ctx);
192        size += self.name.byte_len(ctx);
193        size
194    }
195
196    fn encode<S: Write + ?Sized>(
197        &self,
198        stream: &mut S,
199        ctx: &crate::Context<'_>,
200    ) -> EncodingResult<()> {
201        self.namespace_index.encode(stream, ctx)?;
202        self.name.encode(stream, ctx)
203    }
204}
205impl BinaryDecodable for QualifiedName {
206    fn decode<S: Read + ?Sized>(stream: &mut S, ctx: &crate::Context<'_>) -> EncodingResult<Self> {
207        let namespace_index = u16::decode(stream, ctx)?;
208        let name = UAString::decode(stream, ctx)?;
209        Ok(QualifiedName {
210            namespace_index,
211            name,
212        })
213    }
214}
215
216impl Display for QualifiedName {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        if self.namespace_index > 0 {
219            write!(f, "{}:{}", self.namespace_index, self.name)
220        } else {
221            write!(f, "{}", self.name)
222        }
223    }
224}
225
226static NUMERIC_QNAME_REGEX: LazyLock<Regex> =
227    LazyLock::new(|| Regex::new(r#"^(\d+):(.*)$"#).unwrap());
228
229impl QualifiedName {
230    /// Create a new qualified name from namespace index and name.
231    pub fn new<T>(namespace_index: u16, name: T) -> QualifiedName
232    where
233        T: Into<UAString>,
234    {
235        QualifiedName {
236            namespace_index,
237            name: name.into(),
238        }
239    }
240
241    /// Create a new empty QualifiedName.
242    pub fn null() -> QualifiedName {
243        QualifiedName {
244            namespace_index: 0,
245            name: UAString::null(),
246        }
247    }
248
249    /// Return `true` if this is the null QualifiedName.
250    pub fn is_null(&self) -> bool {
251        self.namespace_index == 0 && self.name.is_null()
252    }
253
254    /// Parse a QualifiedName from a string.
255    /// Note that QualifiedName parsing is unsolvable. This does a best-effort.
256    /// If parsing fails, we will capture the string as a name with namespace index 0.
257    pub fn parse(raw: &str, namespaces: &NamespaceMap) -> QualifiedName {
258        // First, try parsing the string as a numeric QualifiedName.
259        if let Some(caps) = NUMERIC_QNAME_REGEX.captures(raw) {
260            // Ignore errors here, if we fail we fall back on other options.
261            if let Ok(namespace_index) = caps.get(1).unwrap().as_str().parse::<u16>() {
262                let name = caps.get(2).unwrap().as_str();
263                if namespaces
264                    .known_namespaces()
265                    .iter()
266                    .any(|n| n.1 == &namespace_index)
267                {
268                    return QualifiedName::new(namespace_index, name);
269                }
270            }
271        }
272
273        // Next, see if the string contains a semicolon, and if it does, try treating the first half as a URI.
274        if let Some((l, r)) = raw.split_once(";") {
275            if let Ok(l) = percent_decode_str(l) {
276                if let Some(namespace_index) = namespaces.get_index(l.decode_utf8_lossy().as_ref())
277                {
278                    return QualifiedName::new(namespace_index, r);
279                }
280            }
281        }
282
283        QualifiedName::new(0, raw)
284    }
285}