const_oid/
parser.rs

1//! OID string parser with `const` support.
2
3use crate::{Arc, Error, ObjectIdentifier, Result, encoder::Encoder};
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: Option<Arc>,
12
13    /// BER/DER encoder
14    encoder: Encoder<{ ObjectIdentifier::MAX_SIZE }>,
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: None,
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.current_arc {
46                Some(arc) => match self.encoder.arc(arc) {
47                    Ok(encoder) => {
48                        self.encoder = encoder;
49                        Ok(self)
50                    }
51                    Err(err) => Err(err),
52                },
53                None => Err(Error::TrailingDot),
54            },
55            [byte @ b'0'..=b'9', remaining @ ..] => {
56                let digit = byte.saturating_sub(b'0');
57                let arc = match self.current_arc {
58                    Some(arc) => arc,
59                    None => 0,
60                };
61
62                // TODO(tarcieri): use `and_then` when const traits are stable
63                self.current_arc = match arc.checked_mul(10) {
64                    Some(arc) => match arc.checked_add(digit as Arc) {
65                        None => return Err(Error::ArcTooBig),
66                        Some(arc) => Some(arc),
67                    },
68                    None => return Err(Error::ArcTooBig),
69                };
70                self.parse_bytes(remaining)
71            }
72            [b'.', remaining @ ..] => {
73                match self.current_arc {
74                    Some(arc) => {
75                        if remaining.is_empty() {
76                            return Err(Error::TrailingDot);
77                        }
78
79                        // TODO(tarcieri): use `?` when stable in `const fn`
80                        match self.encoder.arc(arc) {
81                            Ok(encoder) => {
82                                self.encoder = encoder;
83                                self.current_arc = None;
84                                self.parse_bytes(remaining)
85                            }
86                            Err(err) => Err(err),
87                        }
88                    }
89                    None => Err(Error::RepeatedDot),
90                }
91            }
92            [byte, ..] => Err(Error::DigitExpected { actual: *byte }),
93        }
94    }
95}
96
97#[cfg(test)]
98#[allow(clippy::unwrap_used)]
99mod tests {
100    use super::Parser;
101    use crate::Error;
102
103    #[test]
104    fn parse() {
105        let oid = Parser::parse("1.23.456").unwrap().finish().unwrap();
106        assert_eq!(oid, "1.23.456".parse().unwrap());
107    }
108
109    #[test]
110    fn reject_empty_string() {
111        assert_eq!(Parser::parse("").err().unwrap(), Error::Empty);
112    }
113
114    #[test]
115    fn reject_non_digits() {
116        assert_eq!(
117            Parser::parse("X").err().unwrap(),
118            Error::DigitExpected { actual: b'X' }
119        );
120
121        assert_eq!(
122            Parser::parse("1.2.X").err().unwrap(),
123            Error::DigitExpected { actual: b'X' }
124        );
125    }
126
127    #[test]
128    fn reject_trailing_dot() {
129        assert_eq!(Parser::parse("1.23.").err().unwrap(), Error::TrailingDot);
130    }
131}