opcua_types/
string.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 `UAString`.
6
7use std::{
8    fmt,
9    io::{Read, Write},
10};
11
12use crate::{
13    encoding::{process_decode_io_result, process_encode_io_result, write_i32, EncodingResult},
14    read_i32, DecodingOptions, Error, OutOfRange, SimpleBinaryDecodable, SimpleBinaryEncodable,
15    UaNullable,
16};
17
18/// To avoid naming conflict hell, the OPC UA String type is typed `UAString` so it does not collide
19/// with the Rust `String`.
20///
21/// A string contains UTF-8 encoded characters or a null value. A null value is distinct from
22/// being an empty string so internally, the code maintains that distinction by holding the value
23/// as an `Option<String>`.
24#[derive(Eq, PartialEq, Debug, Clone, Hash)]
25pub struct UAString {
26    value: Option<String>,
27}
28
29impl fmt::Display for UAString {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        if let Some(ref value) = self.value {
32            write!(f, "{value}")
33        } else {
34            write!(f, "[null]")
35        }
36    }
37}
38
39impl UaNullable for UAString {
40    fn is_ua_null(&self) -> bool {
41        self.is_null()
42    }
43}
44
45#[cfg(feature = "json")]
46mod json {
47    use std::io::{Read, Write};
48    use struson::{
49        reader::{JsonReader, JsonStreamReader, ValueType},
50        writer::{JsonStreamWriter, JsonWriter},
51    };
52
53    use crate::json::{Context, JsonDecodable, JsonEncodable};
54
55    use super::{EncodingResult, UAString};
56
57    impl JsonEncodable for UAString {
58        fn encode(
59            &self,
60            stream: &mut JsonStreamWriter<&mut dyn Write>,
61            _ctx: &Context<'_>,
62        ) -> EncodingResult<()> {
63            if let Some(s) = self.value() {
64                stream.string_value(s)?;
65            } else {
66                stream.null_value()?;
67            }
68
69            Ok(())
70        }
71    }
72
73    impl JsonDecodable for UAString {
74        fn decode(
75            stream: &mut JsonStreamReader<&mut dyn Read>,
76            _ctx: &Context<'_>,
77        ) -> EncodingResult<Self> {
78            match stream.peek()? {
79                ValueType::String => Ok(stream.next_string()?.into()),
80                _ => {
81                    stream.next_null()?;
82                    Ok(UAString::null())
83                }
84            }
85        }
86    }
87}
88
89impl SimpleBinaryEncodable for UAString {
90    fn byte_len(&self) -> usize {
91        // Length plus the actual string length in bytes for a non-null string.
92        4 + match &self.value {
93            Some(s) => s.len(),
94            None => 0,
95        }
96    }
97
98    fn encode<S: Write + ?Sized>(&self, stream: &mut S) -> EncodingResult<()> {
99        // Strings are encoded as UTF8 chars preceded by an Int32 length. A -1 indicates a null string
100        match &self.value {
101            Some(s) => {
102                write_i32(stream, s.len() as i32)?;
103                let buf = s.as_bytes();
104                process_encode_io_result(stream.write_all(buf))
105            }
106            None => write_i32(stream, -1),
107        }
108    }
109}
110
111impl SimpleBinaryDecodable for UAString {
112    fn decode<S: Read + ?Sized>(
113        stream: &mut S,
114        decoding_options: &DecodingOptions,
115    ) -> EncodingResult<Self> {
116        let len = read_i32(stream)?;
117        // Null string?
118        if len == -1 {
119            Ok(UAString::null())
120        } else if len < -1 {
121            Err(Error::decoding(format!(
122                "String buf length is a negative number {len}"
123            )))
124        } else if len as usize > decoding_options.max_string_length {
125            Err(Error::decoding(format!(
126                "String buf length {} exceeds decoding limit {}",
127                len, decoding_options.max_string_length
128            )))
129        } else {
130            // Create a buffer filled with zeroes and read the string over the top
131            let mut buf = vec![0u8; len as usize];
132            process_decode_io_result(stream.read_exact(&mut buf))?;
133            let value = String::from_utf8(buf).map_err(|err| {
134                Error::decoding(format!("Decoded string was not valid UTF-8 - {err}"))
135            })?;
136            Ok(UAString::from(value))
137        }
138    }
139}
140
141#[cfg(feature = "xml")]
142mod xml {
143    use crate::xml::*;
144    use std::io::{Read, Write};
145
146    use super::UAString;
147
148    impl XmlType for UAString {
149        const TAG: &'static str = "String";
150    }
151
152    impl XmlEncodable for UAString {
153        fn encode(
154            &self,
155            writer: &mut XmlStreamWriter<&mut dyn Write>,
156            _ctx: &Context<'_>,
157        ) -> Result<(), Error> {
158            if let Some(s) = self.value() {
159                writer.write_text(s)?;
160            }
161
162            Ok(())
163        }
164    }
165
166    impl XmlDecodable for UAString {
167        fn decode(
168            read: &mut XmlStreamReader<&mut dyn Read>,
169            _context: &Context<'_>,
170        ) -> Result<Self, Error> {
171            Ok(read.consume_as_text()?.into())
172        }
173    }
174}
175
176impl From<UAString> for String {
177    fn from(value: UAString) -> Self {
178        value.as_ref().to_string()
179    }
180}
181
182impl AsRef<str> for UAString {
183    fn as_ref(&self) -> &str {
184        if self.is_null() {
185            ""
186        } else {
187            self.value.as_ref().unwrap()
188        }
189    }
190}
191
192impl<'a> From<&'a str> for UAString {
193    fn from(value: &'a str) -> Self {
194        Self::from(value.to_string())
195    }
196}
197
198impl From<&String> for UAString {
199    fn from(value: &String) -> Self {
200        UAString {
201            value: Some(value.clone()),
202        }
203    }
204}
205
206impl From<String> for UAString {
207    fn from(value: String) -> Self {
208        UAString { value: Some(value) }
209    }
210}
211
212impl From<Option<String>> for UAString {
213    fn from(value: Option<String>) -> Self {
214        UAString { value }
215    }
216}
217
218impl Default for UAString {
219    fn default() -> Self {
220        UAString::null()
221    }
222}
223
224impl PartialEq<str> for UAString {
225    fn eq(&self, other: &str) -> bool {
226        match self.value {
227            None => false,
228            Some(ref v) => v.eq(other),
229        }
230    }
231}
232
233impl UAString {
234    /// Get the inner raw value.
235    pub fn value(&self) -> &Option<String> {
236        &self.value
237    }
238
239    /// Set the inner value.
240    pub fn set_value(&mut self, value: Option<String>) {
241        self.value = value;
242    }
243
244    /// Returns true if the string is null or empty, false otherwise
245    pub fn is_empty(&self) -> bool {
246        self.value.is_none() || self.value.as_ref().is_some_and(|v| v.is_empty())
247    }
248
249    /// Returns the length of the string in bytes or -1 for null.
250    pub fn len(&self) -> isize {
251        if self.value.is_none() {
252            -1
253        } else {
254            self.value.as_ref().unwrap().len() as isize
255        }
256    }
257
258    /// Create a null string (not the same as an empty string).
259    pub fn null() -> UAString {
260        UAString { value: None }
261    }
262
263    /// Test if the string is null.
264    pub fn is_null(&self) -> bool {
265        self.value.is_none()
266    }
267
268    /// This function is meant for use with NumericRange. It creates a substring from this string
269    /// from min up to and inclusive of max. Note that min must have an index within the string
270    /// but max is allowed to be beyond the end in which case the remainder of the string is
271    /// returned (see docs for NumericRange).
272    pub fn substring(&self, min: usize, max: usize) -> Result<UAString, OutOfRange> {
273        if let Some(ref v) = self.value() {
274            if min >= v.len() {
275                Err(OutOfRange)
276            } else {
277                let max = if max >= v.len() { v.len() - 1 } else { max };
278                Ok(UAString::from(&v[min..=max]))
279            }
280        } else {
281            Err(OutOfRange)
282        }
283    }
284}
285
286#[test]
287fn string_null() {
288    let s = UAString::null();
289    assert!(s.is_null());
290    assert!(s.is_empty());
291    assert_eq!(s.len(), -1);
292}
293
294#[test]
295fn string_empty() {
296    let s = UAString::from("");
297    assert!(!s.is_null());
298    assert!(s.is_empty());
299    assert_eq!(s.len(), 0);
300}
301
302#[test]
303fn string_value() {
304    let v = "Mary had a little lamb";
305    let s = UAString::from(v);
306    assert!(!s.is_null());
307    assert!(!s.is_empty());
308    assert_eq!(s.as_ref(), v);
309}
310
311#[test]
312#[allow(clippy::comparison_to_empty)]
313fn string_eq() {
314    let s = UAString::null();
315    assert!(!s.eq(""));
316
317    let s = UAString::from("");
318    assert!(s.eq(""));
319
320    let s = UAString::from("Sunshine");
321    assert!(s.ne("Moonshine"));
322    assert!(s.eq("Sunshine"));
323    assert!(!s.eq("Sunshine "));
324}
325
326#[test]
327fn string_substring() {
328    let a = "Mary had a little lamb";
329    let v = UAString::from(a);
330    let v2 = v.substring(0, 4).unwrap();
331    let a2 = v2.as_ref();
332    assert_eq!(a2, "Mary ");
333
334    let v2 = v.substring(2, 2).unwrap();
335    let a2 = v2.as_ref();
336    assert_eq!(a2, "r");
337
338    let v2 = v.substring(0, 2000).unwrap();
339    assert_eq!(v, v2);
340    assert_eq!(v2.as_ref(), a);
341
342    assert!(v.substring(22, 10000).is_err());
343
344    assert!(UAString::null().substring(0, 0).is_err());
345}