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}