const_oid/
parser.rs

1//! OID string parser with `const` support.
2
3use crate::{encoder::Encoder, Arc, ObjectIdentifier};
4
5/// Const-friendly OID string parser.
6///
7/// Parses an OID from the dotted string representation.
8pub(crate) struct Parser {
9    /// Current arc in progress
10    current_arc: Arc,
11
12    /// BER/DER encoder
13    encoder: Encoder,
14}
15
16impl Parser {
17    /// Parse an OID from a dot-delimited string e.g. `1.2.840.113549.1.1.1`
18    pub(crate) const fn parse(s: &str) -> Self {
19        let bytes = s.as_bytes();
20        const_assert!(!bytes.is_empty(), "OID string is empty");
21        const_assert!(
22            matches!(bytes[0], b'0'..=b'9'),
23            "OID must start with a digit"
24        );
25
26        let current_arc = 0;
27        let encoder = Encoder::new();
28        Self {
29            current_arc,
30            encoder,
31        }
32        .parse_bytes(bytes)
33    }
34
35    /// Finish parsing, returning the result
36    pub(crate) const fn finish(self) -> ObjectIdentifier {
37        self.encoder.finish()
38    }
39
40    /// Parse the remaining bytes
41    const fn parse_bytes(mut self, bytes: &[u8]) -> Self {
42        match bytes {
43            [] => {
44                self.encoder = self.encoder.encode(self.current_arc);
45                self
46            }
47            [byte @ b'0'..=b'9', remaining @ ..] => {
48                let digit = byte.saturating_sub(b'0');
49                self.current_arc = self.current_arc * 10 + digit as Arc;
50                self.parse_bytes(remaining)
51            }
52            [b'.', remaining @ ..] => {
53                const_assert!(!remaining.is_empty(), "invalid trailing '.' in OID");
54                self.encoder = self.encoder.encode(self.current_arc);
55                self.current_arc = 0;
56                self.parse_bytes(remaining)
57            }
58            [byte, ..] => {
59                const_assert!(
60                    matches!(byte, b'0'..=b'9' | b'.'),
61                    "invalid character in OID"
62                );
63
64                // Unreachable (checked by above `const_assert!`)
65                // Needed for match exhaustiveness and matching types
66                self
67            }
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::Parser;
75
76    #[test]
77    fn parse() {
78        let oid = Parser::parse("1.23.456").finish();
79        assert_eq!(oid, "1.23.456".parse().unwrap());
80    }
81
82    #[test]
83    #[should_panic]
84    fn reject_empty_string() {
85        Parser::parse("");
86    }
87
88    #[test]
89    #[should_panic]
90    fn reject_non_digits() {
91        Parser::parse("X");
92    }
93
94    #[test]
95    #[should_panic]
96    fn reject_trailing_dot() {
97        Parser::parse("1.23.");
98    }
99}