const_oid/
parser.rs

1//! OID string parser with `const` support.
2
3use crate::{encoder::Encoder, Arc, Error, ObjectIdentifier, Result};
4
5/// Const-friendly OID string parser.
6///
7/// Parses an OID from the dotted string representation.
8#[derive(Debug)]
9pub(crate) struct Parser {
10    /// Current arc in progress
11    current_arc: Arc,
12
13    /// BER/DER encoder
14    encoder: Encoder,
15}
16
17impl Parser {
18    /// Parse an OID from a dot-delimited string e.g. `1.2.840.113549.1.1.1`
19    pub(crate) const fn parse(s: &str) -> Result<Self> {
20        let bytes = s.as_bytes();
21
22        if bytes.is_empty() {
23            return Err(Error::Empty);
24        }
25
26        match bytes[0] {
27            b'0'..=b'9' => Self {
28                current_arc: 0,
29                encoder: Encoder::new(),
30            }
31            .parse_bytes(bytes),
32            actual => Err(Error::DigitExpected { actual }),
33        }
34    }
35
36    /// Finish parsing, returning the result
37    pub(crate) const fn finish(self) -> Result<ObjectIdentifier> {
38        self.encoder.finish()
39    }
40
41    /// Parse the remaining bytes
42    const fn parse_bytes(mut self, bytes: &[u8]) -> Result<Self> {
43        match bytes {
44            // TODO(tarcieri): use `?` when stable in `const fn`
45            [] => match self.encoder.arc(self.current_arc) {
46                Ok(encoder) => {
47                    self.encoder = encoder;
48                    Ok(self)
49                }
50                Err(err) => Err(err),
51            },
52            // TODO(tarcieri): checked arithmetic
53            #[allow(clippy::integer_arithmetic)]
54            [byte @ b'0'..=b'9', remaining @ ..] => {
55                let digit = byte.saturating_sub(b'0');
56                self.current_arc = self.current_arc * 10 + digit as Arc;
57                self.parse_bytes(remaining)
58            }
59            [b'.', remaining @ ..] => {
60                if remaining.is_empty() {
61                    return Err(Error::TrailingDot);
62                }
63
64                // TODO(tarcieri): use `?` when stable in `const fn`
65                match self.encoder.arc(self.current_arc) {
66                    Ok(encoder) => {
67                        self.encoder = encoder;
68                        self.current_arc = 0;
69                        self.parse_bytes(remaining)
70                    }
71                    Err(err) => Err(err),
72                }
73            }
74            [byte, ..] => Err(Error::DigitExpected { actual: *byte }),
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::Parser;
82    use crate::Error;
83
84    #[test]
85    fn parse() {
86        let oid = Parser::parse("1.23.456").unwrap().finish().unwrap();
87        assert_eq!(oid, "1.23.456".parse().unwrap());
88    }
89
90    #[test]
91    fn reject_empty_string() {
92        assert_eq!(Parser::parse("").err().unwrap(), Error::Empty);
93    }
94
95    #[test]
96    fn reject_non_digits() {
97        assert_eq!(
98            Parser::parse("X").err().unwrap(),
99            Error::DigitExpected { actual: b'X' }
100        );
101
102        assert_eq!(
103            Parser::parse("1.2.X").err().unwrap(),
104            Error::DigitExpected { actual: b'X' }
105        );
106    }
107
108    #[test]
109    fn reject_trailing_dot() {
110        assert_eq!(Parser::parse("1.23.").err().unwrap(), Error::TrailingDot);
111    }
112}