opcua_types/
numeric_range.rs

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