Skip to main content

oi4_dnp_encoding/
validate.rs

1use crate::encode::is_unreserved;
2use crate::error::{Error, ErrorKind};
3use crate::hex::{has_lowercase_hex, is_hex};
4
5/// Validation rule set (runtime adjustable apart from compile-time `strict` feature).
6#[derive(Debug, Clone, Copy, Default)]
7pub struct Rules {
8    /// If true, any ASCII char outside unreserved set must appear only as encoded triplet ",XX".
9    pub enforce_reserved_masking: bool,
10    /// Accept lowercase hex inside escapes when strict feature not active. (Default true)
11    pub allow_lowercase_hex: bool,
12}
13
14impl Rules {
15    pub const fn strict_like() -> Self {
16        Self {
17            enforce_reserved_masking: true,
18            allow_lowercase_hex: false,
19        }
20    }
21}
22
23/// Validate an encoded DNP string against masking rules.
24/// Does not attempt semantic validation beyond escape formatting & reserved usage.
25pub fn validate_dnp(input: &str, rules: &Rules) -> Result<(), Error> {
26    let bytes = input.as_bytes();
27    let mut i = 0usize;
28    while i < bytes.len() {
29        let b = bytes[i];
30        if b == b',' {
31            // Expect two hex digits
32            if i + 2 >= bytes.len() {
33                return Err(Error::new(ErrorKind::LoneComma, Some(i)));
34            }
35            let h1 = bytes[i + 1];
36            let h2 = bytes[i + 2];
37            // strict feature -> forbid lowercase
38            #[cfg(feature = "strict")]
39            {
40                if has_lowercase_hex(h1, h2) {
41                    return Err(Error::new(ErrorKind::LowercaseHexInStrict, Some(i)));
42                }
43            }
44
45            if !rules.allow_lowercase_hex && has_lowercase_hex(h1, h2) {
46                return Err(Error::new(ErrorKind::LowercaseHexInStrict, Some(i)));
47            }
48
49            if !is_hex(h1) {
50                return Err(Error::new(
51                    ErrorKind::InvalidHexDigit(h1 as char),
52                    Some(i + 1),
53                ));
54            }
55            if !is_hex(h2) {
56                return Err(Error::new(
57                    ErrorKind::InvalidHexDigit(h2 as char),
58                    Some(i + 2),
59                ));
60            }
61            i += 3;
62            continue;
63        }
64        if b < 0x80 {
65            // ASCII plain char
66            if rules.enforce_reserved_masking && !is_unreserved(b) {
67                return Err(Error::new(
68                    ErrorKind::UnescapedReservedAscii(b as char),
69                    Some(i),
70                ));
71            }
72            i += 1;
73        } else {
74            // UTF-8 multibyte char boundary
75            // Skip full char
76            let rest = &input[i..];
77            let ch = rest.chars().next().unwrap();
78            i += ch.len_utf8();
79        }
80    }
81    Ok(())
82}