1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//! OID string parser with `const` support.

use crate::{encoder::Encoder, Arc, Error, ObjectIdentifier, Result};

/// Const-friendly OID string parser.
///
/// Parses an OID from the dotted string representation.
#[derive(Debug)]
pub(crate) struct Parser {
    /// Current arc in progress
    current_arc: Arc,

    /// BER/DER encoder
    encoder: Encoder,
}

impl Parser {
    /// Parse an OID from a dot-delimited string e.g. `1.2.840.113549.1.1.1`
    pub(crate) const fn parse(s: &str) -> Result<Self> {
        let bytes = s.as_bytes();

        if bytes.is_empty() {
            return Err(Error::Empty);
        }

        match bytes[0] {
            b'0'..=b'9' => Self {
                current_arc: 0,
                encoder: Encoder::new(),
            }
            .parse_bytes(bytes),
            actual => Err(Error::DigitExpected { actual }),
        }
    }

    /// Finish parsing, returning the result
    pub(crate) const fn finish(self) -> Result<ObjectIdentifier> {
        self.encoder.finish()
    }

    /// Parse the remaining bytes
    const fn parse_bytes(mut self, bytes: &[u8]) -> Result<Self> {
        match bytes {
            // TODO(tarcieri): use `?` when stable in `const fn`
            [] => match self.encoder.arc(self.current_arc) {
                Ok(encoder) => {
                    self.encoder = encoder;
                    Ok(self)
                }
                Err(err) => Err(err),
            },
            // TODO(tarcieri): checked arithmetic
            #[allow(clippy::integer_arithmetic)]
            [byte @ b'0'..=b'9', remaining @ ..] => {
                let digit = byte.saturating_sub(b'0');
                self.current_arc = self.current_arc * 10 + digit as Arc;
                self.parse_bytes(remaining)
            }
            [b'.', remaining @ ..] => {
                if remaining.is_empty() {
                    return Err(Error::TrailingDot);
                }

                // TODO(tarcieri): use `?` when stable in `const fn`
                match self.encoder.arc(self.current_arc) {
                    Ok(encoder) => {
                        self.encoder = encoder;
                        self.current_arc = 0;
                        self.parse_bytes(remaining)
                    }
                    Err(err) => Err(err),
                }
            }
            [byte, ..] => Err(Error::DigitExpected { actual: *byte }),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Parser;
    use crate::Error;

    #[test]
    fn parse() {
        let oid = Parser::parse("1.23.456").unwrap().finish().unwrap();
        assert_eq!(oid, "1.23.456".parse().unwrap());
    }

    #[test]
    fn reject_empty_string() {
        assert_eq!(Parser::parse("").err().unwrap(), Error::Empty);
    }

    #[test]
    fn reject_non_digits() {
        assert_eq!(
            Parser::parse("X").err().unwrap(),
            Error::DigitExpected { actual: b'X' }
        );

        assert_eq!(
            Parser::parse("1.2.X").err().unwrap(),
            Error::DigitExpected { actual: b'X' }
        );
    }

    #[test]
    fn reject_trailing_dot() {
        assert_eq!(Parser::parse("1.23.").err().unwrap(), Error::TrailingDot);
    }
}