opcua_types/
numeric_range.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 `NumericRange`.
6
7use std::{
8    fmt::{self, Display},
9    str::FromStr,
10    sync::LazyLock,
11};
12
13use regex::Regex;
14
15use crate::{impl_encoded_as, Error, UAString, UaNullable};
16
17#[derive(Debug)]
18/// Error returned when parsing a numeric range.
19pub struct NumericRangeError;
20
21impl fmt::Display for NumericRangeError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(f, "NumericRangeError")
24    }
25}
26
27impl std::error::Error for NumericRangeError {}
28
29/// Numeric range describes a range within an array. See OPCUA Part 4 7.22
30///
31/// This parameter is defined in Table 159. A formal BNF definition of the numeric range can be
32/// found in Clause A.3.
33///
34/// The syntax for the string contains one of the following two constructs. The first construct is
35/// the string representation of an individual integer. For example, `6` is valid, but `6,0` and
36/// `3,2` are not. The minimum and maximum values that can be expressed are defined by the use
37/// of this parameter and not by this parameter type definition. The second construct is a range
38/// represented by two integers separated by the colon (`:`) character. The first integer shall
39/// always have a lower value than the second. For example, `5:7` is valid, while `7:5` and `5:5`
40/// are not. The minimum and maximum values that can be expressed by these integers are defined by
41/// the use of this parameter, and not by this parameter type definition. No other characters,
42/// including white-space characters, are permitted.
43///
44/// Multi-dimensional arrays can be indexed by specifying a range for each dimension separated by
45/// a `,`. For example, a 2x2 block in a 4x4 matrix could be selected with the range `1:2,0:1`.
46/// A single element in a multi-dimensional array can be selected by specifying a single number
47/// instead of a range. For example, `1,1` specifies selects the `[1,1]` element in a two dimensional
48/// array.
49///
50/// Dimensions are specified in the order that they appear in the ArrayDimensions Attribute. All
51/// dimensions shall be specified for a NumericRange to be valid.
52///
53/// All indexes start with `0`. The maximum value for any index is one less than the length of the
54/// dimension.
55#[derive(Debug, Clone, PartialEq)]
56pub enum NumericRange {
57    /// None
58    None,
59    /// A single index
60    Index(u32),
61    /// A range of indices
62    Range(u32, u32),
63    /// Multiple ranges contains any mix of Index, Range values - a multiple range containing multiple ranges is invalid
64    MultipleRanges(Vec<NumericRange>),
65}
66
67impl NumericRange {
68    /// Check if this is non-empty.
69    pub fn has_range(&self) -> bool {
70        !self.is_none()
71    }
72
73    /// Check if this range is empty.
74    pub fn is_none(&self) -> bool {
75        matches!(self, NumericRange::None)
76    }
77
78    fn byte_len(&self) -> usize {
79        // String length field = 4 bytes.
80        4 + match self {
81            NumericRange::None => 0,
82            // Length of a decimal number as string.
83            NumericRange::Index(i) => num_len(*i),
84            // Length of two decimal numbers as string, plus colon.
85            NumericRange::Range(l, r) => num_len(*l) + num_len(*r) + 1,
86            NumericRange::MultipleRanges(numeric_ranges) => {
87                numeric_ranges.iter().map(|r| r.byte_len()).sum::<usize>() + numeric_ranges.len()
88                    - 1
89            }
90        }
91    }
92
93    fn from_ua_string(str: UAString) -> Result<Self, Error> {
94        str.as_ref()
95            .parse::<NumericRange>()
96            .map_err(Error::decoding)
97    }
98
99    fn to_ua_string(&self) -> Result<UAString, Error> {
100        match self {
101            NumericRange::None => Ok(UAString::null()),
102            _ => Ok(UAString::from(self.to_string())),
103        }
104    }
105}
106
107impl_encoded_as!(
108    NumericRange,
109    NumericRange::from_ua_string,
110    NumericRange::to_ua_string,
111    NumericRange::byte_len
112);
113
114fn num_len(n: u32) -> usize {
115    if n == 0 {
116        1
117    } else {
118        n.ilog10() as usize + 1
119    }
120}
121
122impl UaNullable for NumericRange {
123    fn is_ua_null(&self) -> bool {
124        self.is_none()
125    }
126}
127
128#[cfg(feature = "xml")]
129impl crate::xml::XmlType for NumericRange {
130    const TAG: &'static str = "NumericRange";
131}
132
133// Valid inputs
134#[test]
135fn valid_numeric_ranges() {
136    let valid_ranges = vec![
137        ("", NumericRange::None, ""),
138        ("0", NumericRange::Index(0), "0"),
139        ("0000", NumericRange::Index(0), "0"),
140        ("1", NumericRange::Index(1), "1"),
141        ("0123456789", NumericRange::Index(123456789), "123456789"),
142        ("4294967295", NumericRange::Index(4294967295), "4294967295"),
143        ("1:2", NumericRange::Range(1, 2), "1:2"),
144        ("2:3", NumericRange::Range(2, 3), "2:3"),
145        (
146            "0:1,0:2,0:3,0:4,0:5",
147            NumericRange::MultipleRanges(vec![
148                NumericRange::Range(0, 1),
149                NumericRange::Range(0, 2),
150                NumericRange::Range(0, 3),
151                NumericRange::Range(0, 4),
152                NumericRange::Range(0, 5),
153            ]),
154            "0:1,0:2,0:3,0:4,0:5",
155        ),
156        (
157            "0:1,2,3,0:4,5,6,7,8,0:9",
158            NumericRange::MultipleRanges(vec![
159                NumericRange::Range(0, 1),
160                NumericRange::Index(2),
161                NumericRange::Index(3),
162                NumericRange::Range(0, 4),
163                NumericRange::Index(5),
164                NumericRange::Index(6),
165                NumericRange::Index(7),
166                NumericRange::Index(8),
167                NumericRange::Range(0, 9),
168            ]),
169            "0:1,2,3,0:4,5,6,7,8,0:9",
170        ),
171    ];
172    for vr in valid_ranges {
173        let range = vr.0.parse::<NumericRange>();
174        if range.is_err() {
175            println!("Range {} is in error when it should be ok", vr.0);
176        }
177        assert!(range.is_ok());
178        assert_eq!(range.unwrap(), vr.1);
179        assert_eq!(vr.2, &vr.1.to_string());
180    }
181}
182
183#[test]
184fn invalid_numeric_ranges() {
185    // Invalid values are either malformed, contain a min >= max, or they exceed limits on size of numbers
186    // or number of indices.
187    let invalid_ranges = vec![
188        " ",
189        " 1",
190        "1 ",
191        ":",
192        ":1",
193        "1:1",
194        "2:1",
195        "0:1,2,3,4:4",
196        "1:",
197        "1:1:2",
198        ",",
199        ":,",
200        ",:",
201        ",1",
202        "1,",
203        "1,2,",
204        "1,,2",
205        "01234567890",
206        "0,1,2,3,4,5,6,7,8,9,10",
207        "4294967296",
208        "0:4294967296",
209        "4294967296:0",
210    ];
211    for vr in invalid_ranges {
212        println!("vr = {vr}");
213        let range = vr.parse::<NumericRange>();
214        if range.is_ok() {
215            println!("Range {vr} is ok when it should be in error");
216        }
217        assert!(range.is_err());
218    }
219}
220
221const MAX_INDICES: usize = 10;
222
223impl FromStr for NumericRange {
224    type Err = NumericRangeError;
225    fn from_str(s: &str) -> Result<Self, Self::Err> {
226        if s.is_empty() {
227            Ok(NumericRange::None)
228        } else {
229            // <numeric-range> ::= <dimension> [',' <dimension>]
230            // <dimension> ::= <index> [':' <index>]
231            // <index> ::= <digit> [<digit>]
232            // <digit> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
233
234            // Split the string on the comma
235            let parts: Vec<_> = s.split(',').collect();
236            match parts.len() {
237                1 => Self::parse_range(parts[0]),
238                2..=MAX_INDICES => {
239                    // Multi dimensions
240                    let mut ranges = Vec::with_capacity(parts.len());
241                    for p in &parts {
242                        if let Ok(range) = Self::parse_range(p) {
243                            ranges.push(range);
244                        } else {
245                            return Err(NumericRangeError);
246                        }
247                    }
248                    Ok(NumericRange::MultipleRanges(ranges))
249                }
250                // 0 parts, or more than MAX_INDICES (really????)
251                _ => Err(NumericRangeError),
252            }
253        }
254    }
255}
256
257impl Display for NumericRange {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        match self {
260            NumericRange::None => write!(f, ""),
261            NumericRange::Index(idx) => write!(f, "{idx}"),
262            NumericRange::Range(min, max) => write!(f, "{min}:{max}"),
263            NumericRange::MultipleRanges(vec) => {
264                let mut needs_comma = false;
265                for r in vec {
266                    if needs_comma {
267                        write!(f, ",")?;
268                    }
269                    needs_comma = true;
270                    write!(f, "{r}")?;
271                }
272                Ok(())
273            }
274        }
275    }
276}
277
278impl NumericRange {
279    /// Parse a numeric range from a string.
280    pub fn new(s: &str) -> Result<Self, NumericRangeError> {
281        Self::from_str(s)
282    }
283
284    fn parse_range(s: &str) -> Result<NumericRange, NumericRangeError> {
285        if s.is_empty() {
286            Err(NumericRangeError)
287        } else {
288            // Regex checks for number or number:number
289            //
290            // The BNF for numeric range doesn't appear to care that number could start with a zero,
291            // e.g. 0009 etc. or have any limits on length.
292            //
293            // To stop insane values, a number must be 10 digits (sufficient for any permissible
294            // 32-bit value) or less regardless of leading zeroes.
295            static RE: LazyLock<Regex> = LazyLock::new(|| {
296                Regex::new("^(?P<min>[0-9]{1,10})(:(?P<max>[0-9]{1,10}))?$").unwrap()
297            });
298            if let Some(captures) = RE.captures(s) {
299                let min = captures.name("min");
300                let max = captures.name("max");
301                match (min, max) {
302                    (None, None) | (None, Some(_)) => Err(NumericRangeError),
303                    (Some(min), None) => min
304                        .as_str()
305                        .parse::<u32>()
306                        .map(NumericRange::Index)
307                        .map_err(|_| NumericRangeError),
308                    (Some(min), Some(max)) => {
309                        // Parse as 64-bit but cast down
310                        if let Ok(min) = min.as_str().parse::<u64>() {
311                            if let Ok(max) = max.as_str().parse::<u64>() {
312                                if min >= max || max > u32::MAX as u64 {
313                                    Err(NumericRangeError)
314                                } else {
315                                    Ok(NumericRange::Range(min as u32, max as u32))
316                                }
317                            } else {
318                                Err(NumericRangeError)
319                            }
320                        } else {
321                            Err(NumericRangeError)
322                        }
323                    }
324                }
325            } else {
326                Err(NumericRangeError)
327            }
328        }
329    }
330
331    /// Tests if the range is basically valid, i.e. that the min < max, that multiple ranges
332    /// doesn't point to multiple ranges
333    pub fn is_valid(&self) -> bool {
334        match self {
335            NumericRange::None => true,
336            NumericRange::Index(_) => true,
337            NumericRange::Range(min, max) => min < max,
338            NumericRange::MultipleRanges(ref ranges) => {
339                let found_invalid = ranges.iter().any(|r| {
340                    // Nested multiple ranges are not allowed
341                    match r {
342                        NumericRange::MultipleRanges(_) => true,
343                        r => !r.is_valid(),
344                    }
345                });
346                !found_invalid
347            }
348        }
349    }
350}