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