opcua_types/
expanded_node_id.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2024 Adam Lock
4
5//! Contains the implementation of `ExpandedNodeId`.
6
7use std::{
8    self,
9    borrow::Cow,
10    fmt,
11    io::{Read, Write},
12    str::FromStr,
13    sync::LazyLock,
14};
15
16use crate::{
17    byte_string::ByteString,
18    encoding::{BinaryDecodable, BinaryEncodable, EncodingResult},
19    guid::Guid,
20    node_id::{Identifier, NodeId},
21    read_u16, read_u32, read_u8,
22    status_code::StatusCode,
23    string::*,
24    write_u16, write_u32, write_u8, Context, Error, NamespaceMap, UaNullable,
25};
26
27/// A NodeId that allows the namespace URI to be specified instead of an index.
28#[derive(PartialEq, Debug, Clone, Eq, Hash, Default)]
29pub struct ExpandedNodeId {
30    /// The inner NodeId.
31    pub node_id: NodeId,
32    /// The full namespace URI. If this is set, the node ID namespace index may be zero.
33    pub namespace_uri: UAString,
34    /// The server index. 0 means current server.
35    pub server_index: u32,
36}
37
38impl UaNullable for ExpandedNodeId {
39    fn is_ua_null(&self) -> bool {
40        self.is_null()
41    }
42}
43
44#[cfg(feature = "json")]
45mod json {
46    // JSON serialization schema as per spec:
47    //
48    // "Type"
49    //      The IdentifierType encoded as a JSON number.
50    //      Allowed values are:
51    //            0 - UInt32 Identifier encoded as a JSON number.
52    //            1 - A String Identifier encoded as a JSON string.
53    //            2 - A Guid Identifier encoded as described in 5.4.2.7.
54    //            3 - A ByteString Identifier encoded as described in 5.4.2.8.
55    //      This field is omitted for UInt32 identifiers.
56    // "Id"
57    //      The Identifier.
58    //      The value of the id field specifies the encoding of this field.
59    // "Namespace"
60    //      The NamespaceIndex for the NodeId.
61    //      The field is encoded as a JSON number for the reversible encoding.
62    //      The field is omitted if the NamespaceIndex equals 0.
63    //      For the non-reversible encoding, the field is the NamespaceUri associated with the NamespaceIndex, encoded as a JSON string.
64    //      A NamespaceIndex of 1 is always encoded as a JSON number.
65    // "ServerUri"
66    //      The ServerIndex for the ExpandedNodeId.
67    //      This field is encoded as a JSON number for the reversible encoding.
68    //      This field is omitted if the ServerIndex equals 0.
69    //      For the non-reversible encoding, this field is the ServerUri associated with the ServerIndex portion of the ExpandedNodeId, encoded as a JSON string.
70
71    use std::io::{Read, Write};
72    use std::str::FromStr;
73
74    use crate::{json::*, ByteString, Error, Guid};
75
76    use super::{ExpandedNodeId, Identifier, NodeId, UAString};
77    enum RawIdentifier {
78        String(String),
79        Integer(u32),
80    }
81
82    impl JsonEncodable for ExpandedNodeId {
83        fn encode(
84            &self,
85            stream: &mut JsonStreamWriter<&mut dyn Write>,
86            ctx: &crate::json::Context<'_>,
87        ) -> super::EncodingResult<()> {
88            stream.begin_object()?;
89            match &self.node_id.identifier {
90                super::Identifier::Numeric(n) => {
91                    stream.name("Id")?;
92                    stream.number_value(*n)?;
93                }
94                super::Identifier::String(uastring) => {
95                    stream.name("IdType")?;
96                    stream.number_value(1)?;
97                    stream.name("Id")?;
98                    JsonEncodable::encode(uastring, stream, ctx)?;
99                }
100                super::Identifier::Guid(guid) => {
101                    stream.name("IdType")?;
102                    stream.number_value(2)?;
103                    stream.name("Id")?;
104                    JsonEncodable::encode(guid, stream, ctx)?;
105                }
106                super::Identifier::ByteString(byte_string) => {
107                    stream.name("IdType")?;
108                    stream.number_value(3)?;
109                    stream.name("Id")?;
110                    JsonEncodable::encode(byte_string, stream, ctx)?;
111                }
112            }
113            if !self.namespace_uri.is_null() {
114                stream.name("Namespace")?;
115                stream.string_value(self.namespace_uri.as_ref())?;
116            } else if self.node_id.namespace != 0 {
117                stream.name("Namespace")?;
118                stream.number_value(self.node_id.namespace)?;
119            }
120            if self.server_index != 0 {
121                stream.name("ServerUri")?;
122                stream.number_value(self.server_index)?;
123            }
124            stream.end_object()?;
125            Ok(())
126        }
127    }
128
129    impl JsonDecodable for ExpandedNodeId {
130        fn decode(
131            stream: &mut JsonStreamReader<&mut dyn Read>,
132            _ctx: &Context<'_>,
133        ) -> super::EncodingResult<Self> {
134            match stream.peek()? {
135                ValueType::Null => {
136                    stream.next_null()?;
137                    return Ok(Self::null());
138                }
139                _ => stream.begin_object()?,
140            }
141
142            let mut id_type: Option<u16> = None;
143            let mut namespace: Option<RawIdentifier> = None;
144            let mut value: Option<RawIdentifier> = None;
145            let mut server_uri: Option<u32> = None;
146
147            while stream.has_next()? {
148                match stream.next_name()? {
149                    "IdType" => {
150                        id_type = Some(stream.next_number()??);
151                    }
152                    "Namespace" => match stream.peek()? {
153                        ValueType::Null => {
154                            stream.next_null()?;
155                            namespace = Some(RawIdentifier::Integer(0));
156                        }
157                        ValueType::Number => {
158                            namespace = Some(RawIdentifier::Integer(stream.next_number()??));
159                        }
160                        _ => {
161                            namespace = Some(RawIdentifier::String(stream.next_string()?));
162                        }
163                    },
164                    "ServerUri" => {
165                        server_uri = Some(stream.next_number()??);
166                    }
167                    "Id" => match stream.peek()? {
168                        ValueType::Null => {
169                            stream.next_null()?;
170                            value = Some(RawIdentifier::Integer(0));
171                        }
172                        ValueType::Number => {
173                            value = Some(RawIdentifier::Integer(stream.next_number()??));
174                        }
175                        _ => {
176                            value = Some(RawIdentifier::String(stream.next_string()?));
177                        }
178                    },
179                    _ => stream.skip_value()?,
180                }
181            }
182
183            let identifier = match id_type {
184                Some(1) => {
185                    let Some(RawIdentifier::String(s)) = value else {
186                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
187                    };
188                    let s = UAString::from(s);
189                    if s.is_null() || s.is_empty() {
190                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
191                    }
192                    Identifier::String(s)
193                }
194                Some(2) => {
195                    let Some(RawIdentifier::String(s)) = value else {
196                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
197                    };
198                    let s = Guid::from_str(&s)
199                        .map_err(|_| Error::decoding("Unable to decode GUID identifier"))?;
200                    Identifier::Guid(s)
201                }
202                Some(3) => {
203                    let Some(RawIdentifier::String(s)) = value else {
204                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
205                    };
206                    let s: ByteString = ByteString::from_base64(&s)
207                        .ok_or_else(|| Error::decoding("Unable to decode bytestring identifier"))?;
208                    Identifier::ByteString(s)
209                }
210                None | Some(0) => {
211                    let Some(RawIdentifier::Integer(s)) = value else {
212                        return Err(Error::decoding("Invalid NodeId, empty identifier"));
213                    };
214                    Identifier::Numeric(s)
215                }
216                Some(r) => {
217                    return Err(Error::decoding(format!(
218                        "Failed to deserialize NodeId, got unexpected IdType {r}"
219                    )));
220                }
221            };
222
223            let (namespace_uri, namespace) = match namespace {
224                Some(RawIdentifier::String(s)) => (Some(s), 0u16),
225                Some(RawIdentifier::Integer(s)) => (None, s.try_into().map_err(Error::decoding)?),
226                None => (None, 0),
227            };
228
229            stream.end_object()?;
230            Ok(ExpandedNodeId {
231                node_id: NodeId {
232                    namespace,
233                    identifier,
234                },
235                namespace_uri: namespace_uri.into(),
236                server_index: server_uri.unwrap_or_default(),
237            })
238        }
239    }
240}
241
242#[cfg(feature = "xml")]
243mod xml {
244    // ExpandedNodeId in XML is for some reason just the exact same
245    // as a NodeId.
246    use crate::{xml::*, NodeId, UAString};
247    use std::io::{Read, Write};
248
249    use super::ExpandedNodeId;
250
251    impl XmlType for ExpandedNodeId {
252        const TAG: &'static str = "ExpandedNodeId";
253    }
254
255    impl XmlEncodable for ExpandedNodeId {
256        fn encode(
257            &self,
258            writer: &mut XmlStreamWriter<&mut dyn Write>,
259            context: &Context<'_>,
260        ) -> EncodingResult<()> {
261            let Some(node_id) = context.namespaces().resolve_node_id(self) else {
262                return Err(Error::encoding(
263                    "Unable to resolve ExpandedNodeId, invalid namespace",
264                ));
265            };
266            node_id.encode(writer, context)
267        }
268    }
269
270    impl XmlDecodable for ExpandedNodeId {
271        fn decode(
272            reader: &mut XmlStreamReader<&mut dyn Read>,
273            context: &Context<'_>,
274        ) -> EncodingResult<Self> {
275            let node_id = NodeId::decode(reader, context)?;
276            Ok(ExpandedNodeId {
277                node_id,
278                namespace_uri: UAString::null(),
279                server_index: 0,
280            })
281        }
282    }
283}
284
285impl BinaryEncodable for ExpandedNodeId {
286    fn byte_len(&self, ctx: &crate::Context<'_>) -> usize {
287        let mut size = self.node_id.byte_len(ctx);
288        if !self.namespace_uri.is_null() {
289            size += self.namespace_uri.byte_len(ctx);
290        }
291        if self.server_index != 0 {
292            size += self.server_index.byte_len(ctx);
293        }
294        size
295    }
296
297    fn encode<S: Write + ?Sized>(&self, stream: &mut S, ctx: &Context<'_>) -> EncodingResult<()> {
298        let mut data_encoding = 0;
299        if !self.namespace_uri.is_null() {
300            data_encoding |= 0x80;
301        }
302        if self.server_index != 0 {
303            data_encoding |= 0x40;
304        }
305
306        // Type determines the byte code
307        match &self.node_id.identifier {
308            Identifier::Numeric(value) => {
309                if self.node_id.namespace == 0 && *value <= 255 {
310                    // node id fits into 2 bytes when the namespace is 0 and the value <= 255
311                    write_u8(stream, data_encoding)?;
312                    write_u8(stream, *value as u8)?;
313                } else if self.node_id.namespace <= 255 && *value <= 65535 {
314                    // node id fits into 4 bytes when namespace <= 255 and value <= 65535
315                    write_u8(stream, data_encoding | 0x1)?;
316                    write_u8(stream, self.node_id.namespace as u8)?;
317                    write_u16(stream, *value as u16)?;
318                } else {
319                    // full node id
320                    write_u8(stream, data_encoding | 0x2)?;
321                    write_u16(stream, self.node_id.namespace)?;
322                    write_u32(stream, *value)?;
323                }
324            }
325            Identifier::String(value) => {
326                write_u8(stream, data_encoding | 0x3)?;
327                write_u16(stream, self.node_id.namespace)?;
328                value.encode(stream, ctx)?;
329            }
330            Identifier::Guid(value) => {
331                write_u8(stream, data_encoding | 0x4)?;
332                write_u16(stream, self.node_id.namespace)?;
333                value.encode(stream, ctx)?;
334            }
335            Identifier::ByteString(ref value) => {
336                write_u8(stream, data_encoding | 0x5)?;
337                write_u16(stream, self.node_id.namespace)?;
338                value.encode(stream, ctx)?;
339            }
340        }
341        if !self.namespace_uri.is_null() {
342            self.namespace_uri.encode(stream, ctx)?;
343        }
344        if self.server_index != 0 {
345            self.server_index.encode(stream, ctx)?;
346        }
347        Ok(())
348    }
349}
350
351impl BinaryDecodable for ExpandedNodeId {
352    fn decode<S: Read + ?Sized>(stream: &mut S, ctx: &Context<'_>) -> EncodingResult<Self> {
353        let data_encoding = read_u8(stream)?;
354        let identifier = data_encoding & 0x0f;
355        let node_id = match identifier {
356            0x0 => {
357                let value = read_u8(stream)?;
358                NodeId::new(0, u32::from(value))
359            }
360            0x1 => {
361                let namespace = read_u8(stream)?;
362                let value = read_u16(stream)?;
363                NodeId::new(u16::from(namespace), u32::from(value))
364            }
365            0x2 => {
366                let namespace = read_u16(stream)?;
367                let value = read_u32(stream)?;
368                NodeId::new(namespace, value)
369            }
370            0x3 => {
371                let namespace = read_u16(stream)?;
372                let value = UAString::decode(stream, ctx)?;
373                NodeId::new(namespace, value)
374            }
375            0x4 => {
376                let namespace = read_u16(stream)?;
377                let value = Guid::decode(stream, ctx)?;
378                NodeId::new(namespace, value)
379            }
380            0x5 => {
381                let namespace = read_u16(stream)?;
382                let value = ByteString::decode(stream, ctx)?;
383                NodeId::new(namespace, value)
384            }
385            _ => {
386                return Err(Error::encoding(format!(
387                    "Unrecognized expanded node id type {identifier}"
388                )));
389            }
390        };
391
392        // Optional stuff
393        let namespace_uri = if data_encoding & 0x80 != 0 {
394            UAString::decode(stream, ctx)?
395        } else {
396            UAString::null()
397        };
398        let server_index = if data_encoding & 0x40 != 0 {
399            u32::decode(stream, ctx)?
400        } else {
401            0
402        };
403
404        Ok(ExpandedNodeId {
405            node_id,
406            namespace_uri,
407            server_index,
408        })
409    }
410}
411
412impl From<&NodeId> for ExpandedNodeId {
413    fn from(value: &NodeId) -> Self {
414        value.clone().into()
415    }
416}
417
418impl From<(NodeId, u32)> for ExpandedNodeId {
419    fn from(v: (NodeId, u32)) -> Self {
420        ExpandedNodeId {
421            node_id: v.0,
422            namespace_uri: UAString::null(),
423            server_index: v.1,
424        }
425    }
426}
427
428impl<T> From<(T, &str)> for ExpandedNodeId
429where
430    T: Into<NodeId>,
431{
432    fn from(value: (T, &str)) -> Self {
433        ExpandedNodeId {
434            node_id: value.0.into(),
435            namespace_uri: value.1.into(),
436            server_index: 0,
437        }
438    }
439}
440
441impl From<NodeId> for ExpandedNodeId {
442    fn from(v: NodeId) -> Self {
443        ExpandedNodeId {
444            node_id: v,
445            namespace_uri: UAString::null(),
446            server_index: 0,
447        }
448    }
449}
450
451impl fmt::Display for ExpandedNodeId {
452    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
453        // Formatted depending on the namespace uri being empty or not.
454        if self.namespace_uri.is_empty() {
455            // svr=<serverindex>;ns=<namespaceindex>;<type>=<value>
456            write!(f, "svr={};{}", self.server_index, self.node_id)
457        } else {
458            // The % and ; chars have to be escaped out in the uri
459            let namespace_uri = String::from(self.namespace_uri.as_ref())
460                .replace('%', "%25")
461                .replace(';', "%3b");
462            // svr=<serverindex>;nsu=<uri>;<type>=<value>
463            write!(
464                f,
465                "svr={};nsu={};{}",
466                self.server_index, namespace_uri, self.node_id.identifier
467            )
468        }
469    }
470}
471
472impl FromStr for ExpandedNodeId {
473    type Err = StatusCode;
474    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
475        use regex::Regex;
476
477        // Parses a node from a string using the format specified in 5.3.1.11 part 6
478        //
479        // svr=<serverindex>;ns=<namespaceindex>;<type>=<value>
480        // or
481        // svr=<serverindex>;nsu=<uri>;<type>=<value>
482
483        static RE: LazyLock<Regex> = LazyLock::new(|| {
484            Regex::new(
485                r"^svr=(?P<svr>[0-9]+);(ns=(?P<ns>[0-9]+)|nsu=(?P<nsu>[^;]+));(?P<t>[isgb]=.+)$",
486            )
487            .unwrap()
488        });
489
490        let captures = RE.captures(s).ok_or(StatusCode::BadNodeIdInvalid)?;
491
492        // Server index
493        let server_index = captures
494            .name("svr")
495            .ok_or(StatusCode::BadNodeIdInvalid)
496            .and_then(|server_index| {
497                server_index
498                    .as_str()
499                    .parse::<u32>()
500                    .map_err(|_| StatusCode::BadNodeIdInvalid)
501            })?;
502
503        // Check for namespace uri
504        let namespace_uri = if let Some(nsu) = captures.name("nsu") {
505            // The % and ; chars need to be unescaped
506            let nsu = String::from(nsu.as_str())
507                .replace("%3b", ";")
508                .replace("%25", "%");
509            UAString::from(nsu)
510        } else {
511            UAString::null()
512        };
513
514        let namespace = if let Some(ns) = captures.name("ns") {
515            ns.as_str()
516                .parse::<u16>()
517                .map_err(|_| StatusCode::BadNodeIdInvalid)?
518        } else {
519            0
520        };
521
522        // Type identifier
523        let t = captures.name("t").unwrap();
524        Identifier::from_str(t.as_str())
525            .map(|t| ExpandedNodeId {
526                server_index,
527                namespace_uri,
528                node_id: NodeId::new(namespace, t),
529            })
530            .map_err(|_| StatusCode::BadNodeIdInvalid)
531    }
532}
533
534impl ExpandedNodeId {
535    /// Creates an expanded node id from a node id
536    pub fn new<T>(value: T) -> ExpandedNodeId
537    where
538        T: 'static + Into<ExpandedNodeId>,
539    {
540        value.into()
541    }
542
543    /// Creates an expanded node id from a namespace URI and an identifier.
544    pub fn new_with_namespace(namespace: &str, value: impl Into<Identifier> + 'static) -> Self {
545        Self {
546            namespace_uri: namespace.into(),
547            node_id: NodeId::new(0, value),
548            server_index: 0,
549        }
550    }
551
552    /// Return a null ExpandedNodeId.
553    pub fn null() -> ExpandedNodeId {
554        Self::new(NodeId::null())
555    }
556
557    /// Return `true` if this expanded node ID is null.
558    pub fn is_null(&self) -> bool {
559        self.node_id.is_null()
560    }
561
562    /// Try to resolve the expanded node ID into a NodeId.
563    /// This will directly return the inner NodeId if namespace URI is null, otherwise it will
564    /// try to return a NodeId with the namespace index given by the namespace uri.
565    /// If server index is non-zero, this will always return None, otherwise, it will return
566    /// None if the namespace is not in the namespace map.
567    pub fn try_resolve<'a>(&'a self, namespaces: &NamespaceMap) -> Option<Cow<'a, NodeId>> {
568        if self.server_index != 0 {
569            return None;
570        }
571        if let Some(uri) = self.namespace_uri.value() {
572            let idx = namespaces.get_index(uri)?;
573            Some(Cow::Owned(NodeId {
574                namespace: idx,
575                identifier: self.node_id.identifier.clone(),
576            }))
577        } else {
578            Some(Cow::Borrowed(&self.node_id))
579        }
580    }
581}